mcpower-proxy 0.0.72__tar.gz → 0.0.81__tar.gz

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.
Files changed (69) hide show
  1. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/PKG-INFO +3 -2
  2. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/pyproject.toml +23 -3
  3. mcpower_proxy-0.0.81/src/ide_tools/common/__init__.py +5 -0
  4. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/__init__.py +5 -0
  5. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/init.py +130 -0
  6. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/output.py +63 -0
  7. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/prompt_submit.py +136 -0
  8. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/read_file.py +170 -0
  9. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/shell_execution.py +289 -0
  10. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/shell_parser_bashlex.py +394 -0
  11. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/types.py +34 -0
  12. mcpower_proxy-0.0.81/src/ide_tools/common/hooks/utils.py +286 -0
  13. mcpower_proxy-0.0.81/src/ide_tools/cursor/constants.py +77 -0
  14. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/ide_tools/cursor/router.py +10 -3
  15. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/ide_tools/router.py +4 -1
  16. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/PKG-INFO +3 -2
  17. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/SOURCES.txt +12 -0
  18. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/requires.txt +2 -1
  19. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/logs/audit_trail.py +5 -4
  20. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/redaction/gitleaks_rules.py +2 -2
  21. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/redaction/pii_rules.py +0 -48
  22. mcpower_proxy-0.0.81/src/modules/utils/platform.py +23 -0
  23. mcpower_proxy-0.0.81/src/modules/utils/string.py +17 -0
  24. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/wrapper/__version__.py +1 -1
  25. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/wrapper/middleware.py +23 -10
  26. mcpower_proxy-0.0.72/src/ide_tools/cursor/constants.py +0 -58
  27. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/LICENSE +0 -0
  28. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/README.md +0 -0
  29. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/setup.cfg +0 -0
  30. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/LICENSE +0 -0
  31. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/ide_tools/__init__.py +0 -0
  32. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/ide_tools/cursor/__init__.py +0 -0
  33. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/ide_tools/cursor/format.py +0 -0
  34. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/main.py +0 -0
  35. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/dependency_links.txt +0 -0
  36. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/entry_points.txt +0 -0
  37. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/mcpower_proxy.egg-info/top_level.txt +0 -0
  38. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/__init__.py +0 -0
  39. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/apis/__init__.py +0 -0
  40. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/apis/security_policy.py +0 -0
  41. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/decision_handler.py +0 -0
  42. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/logs/__init__.py +0 -0
  43. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/logs/logger.py +0 -0
  44. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/redaction/__init__.py +0 -0
  45. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/redaction/constants.py +0 -0
  46. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/redaction/redactor.py +0 -0
  47. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/__init__.py +0 -0
  48. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/classes.py +0 -0
  49. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/confirmation.py +0 -0
  50. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/simple_dialog.py +0 -0
  51. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/__init__.py +0 -0
  52. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/constants.py +0 -0
  53. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/mac_dialogs.py +0 -0
  54. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/tk_dialogs.py +0 -0
  55. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/windows_custom_dialog.py +0 -0
  56. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/windows_dialogs.py +0 -0
  57. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/windows_structs.py +0 -0
  58. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/yad_dialogs.py +0 -0
  59. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/ui/xdialog/zenity_dialogs.py +0 -0
  60. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/__init__.py +0 -0
  61. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/cli.py +0 -0
  62. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/config.py +0 -0
  63. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/copy.py +0 -0
  64. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/ids.py +0 -0
  65. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/json.py +0 -0
  66. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/modules/utils/mcp_configs.py +0 -0
  67. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/wrapper/__init__.py +0 -0
  68. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/wrapper/schema.py +0 -0
  69. {mcpower_proxy-0.0.72 → mcpower_proxy-0.0.81}/src/wrapper/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpower-proxy
3
- Version: 0.0.72
3
+ Version: 0.0.81
4
4
  Summary: MCPower Security proxy
5
5
  Author-email: MCPower Security <support@mcpower.tech>
6
6
  License: Apache License
@@ -216,7 +216,8 @@ Requires-Dist: watchdog>=3.0.0
216
216
  Requires-Dist: jsonc-parser>=1.1.5
217
217
  Requires-Dist: jsonpath-ng>=1.7.0
218
218
  Requires-Dist: pydantic>=2.8.0
219
- Requires-Dist: mcpower-shared==0.1.3
219
+ Requires-Dist: mcpower-shared==0.1.5
220
+ Requires-Dist: bashlex>=0.18
220
221
  Dynamic: license-file
221
222
 
222
223
  # MCPower Proxy
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcpower-proxy"
3
- version = "0.0.72"
3
+ version = "0.0.81"
4
4
  description = "MCPower Security proxy"
5
5
  readme = "README.md"
6
6
  requires-python = "~=3.11.0"
@@ -16,7 +16,8 @@ dependencies = [
16
16
  "jsonc-parser>=1.1.5",
17
17
  "jsonpath-ng>=1.7.0",
18
18
  "pydantic>=2.8.0",
19
- "mcpower-shared==0.1.3",
19
+ "mcpower-shared==0.1.5",
20
+ "bashlex>=0.18",
20
21
  ]
21
22
 
22
23
  [project.license]
@@ -31,9 +32,28 @@ build-backend = "setuptools.build_meta"
31
32
 
32
33
  [tool.setuptools]
33
34
  package-dir = {"" = "src"}
34
- packages = ["modules", "modules.apis", "modules.logs", "modules.redaction", "modules.ui", "modules.ui.xdialog", "modules.utils", "wrapper", "ide_tools", "ide_tools.cursor"]
35
+ packages = [
36
+ "modules",
37
+ "modules.apis",
38
+ "modules.logs",
39
+ "modules.redaction",
40
+ "modules.ui",
41
+ "modules.ui.xdialog",
42
+ "modules.utils",
43
+ "wrapper",
44
+ "ide_tools",
45
+ "ide_tools.common",
46
+ "ide_tools.common.hooks",
47
+ "ide_tools.cursor"
48
+ ]
35
49
  py-modules = ["main"]
36
50
 
51
+ [dependency-groups]
52
+ dev = [
53
+ "pytest>=8.0.0",
54
+ "pytest-anyio>=0.0.0",
55
+ ]
56
+
37
57
  [tool.uv]
38
58
 
39
59
  [tool.pytest.ini_options]
@@ -0,0 +1,5 @@
1
+ """
2
+ Common IDE Tools
3
+
4
+ Shared logic for all IDE integrations.
5
+ """
@@ -0,0 +1,5 @@
1
+ """
2
+ Common Hook Handlers
3
+
4
+ Shared hook logic for all IDEs
5
+ """
@@ -0,0 +1,130 @@
1
+ """
2
+ Shared initialization logic for IDE tools
3
+
4
+ Registers IDE hooks with the security API.
5
+ """
6
+
7
+ import sys
8
+ from typing import Dict, Optional
9
+
10
+ from mcpower_shared.mcp_types import InitRequest, EnvironmentContext, ServerRef, ToolRef
11
+ from modules.apis.security_policy import SecurityPolicyClient
12
+ from modules.logs.audit_trail import AuditTrailLogger
13
+ from modules.logs.logger import MCPLogger
14
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
15
+ from modules.utils.json import safe_json_dumps
16
+ from wrapper.__version__ import __version__
17
+
18
+
19
+ def output_init_result(success: bool, message: str):
20
+ """
21
+ Output init result to stdout
22
+
23
+ Args:
24
+ success: True if initialization succeeded
25
+ message: Status message
26
+ """
27
+ result = {
28
+ "success": success,
29
+ "message": message
30
+ }
31
+
32
+ print(safe_json_dumps(result), flush=True)
33
+
34
+
35
+ async def handle_init(
36
+ logger: MCPLogger,
37
+ audit_logger: AuditTrailLogger,
38
+ event_id: str,
39
+ prompt_id: str,
40
+ cwd: Optional[str],
41
+ server_name: str,
42
+ client_name: str,
43
+ hooks: Dict[str, Dict[str, str]]
44
+ ) -> None:
45
+ """
46
+ Generic init handler - registers hooks with security API
47
+
48
+ Args:
49
+ logger: Logger instance
50
+ audit_logger: Audit logger instance
51
+ event_id: Event identifier
52
+ prompt_id: Prompt identifier
53
+ cwd: Current working directory
54
+ server_name: IDE-specific server name (e.g. "cursor_tools_mcp")
55
+ client_name: IDE-specific client name (e.g. "cursor", "claude-code")
56
+ hooks: Dict of hook definitions with {name, description, version}
57
+
58
+ Outputs result and exits with appropriate code.
59
+ """
60
+ session_id = get_session_id()
61
+
62
+ logger.info(f"Init handler started (client={client_name}, event_id={event_id}, prompt_id={prompt_id}, cwd={cwd})")
63
+
64
+ try:
65
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
66
+ audit_logger.set_app_uid(app_uid)
67
+
68
+ audit_logger.log_event(
69
+ "mcpower_start",
70
+ {
71
+ "wrapper_version": __version__,
72
+ "wrapped_server_name": server_name,
73
+ "client": client_name
74
+ }
75
+ )
76
+
77
+ try:
78
+ tools = [
79
+ ToolRef(
80
+ name=hook_info["name"],
81
+ description=f"Description:\n{hook_info['description']}\n\n"
82
+ f"inputSchema:\n{hook_info['parameters']}",
83
+ version=hook_info["version"]
84
+ )
85
+ for hook_info in hooks.values()
86
+ ]
87
+
88
+ init_request = InitRequest(
89
+ environment=EnvironmentContext(
90
+ session_id=session_id,
91
+ workspace={
92
+ "roots": [cwd] if cwd else [],
93
+ "current_files": []
94
+ },
95
+ client=client_name,
96
+ client_version=__version__,
97
+ selection_hash=""
98
+ ),
99
+ server=ServerRef(
100
+ name=server_name,
101
+ transport="stdio",
102
+ version="1.0.0",
103
+ context="ide"
104
+ ),
105
+ tools=tools
106
+ )
107
+
108
+ async with SecurityPolicyClient(
109
+ session_id=session_id,
110
+ logger=logger,
111
+ audit_logger=audit_logger,
112
+ app_id=app_uid
113
+ ) as client:
114
+ await client.init_tools(init_request, event_id=event_id)
115
+
116
+ logger.info(f"Hooks registered successfully for {client_name}")
117
+
118
+ # Success - output result and exit
119
+ output_init_result(True, f"{client_name.title()} hooks registered successfully")
120
+ sys.exit(0)
121
+
122
+ except Exception as e:
123
+ logger.error(f"API initialization failed: {e}")
124
+ output_init_result(False, f"Error: {str(e)}")
125
+ sys.exit(1)
126
+
127
+ except Exception as e:
128
+ logger.error(f"Unexpected error in init handler: {e}", exc_info=True)
129
+ output_init_result(False, f"Initialization failed: {str(e)}")
130
+ sys.exit(1)
@@ -0,0 +1,63 @@
1
+ """
2
+ IDE-agnostic output handling
3
+ """
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from modules.logs.logger import MCPLogger
9
+ from .types import OutputFormat
10
+
11
+
12
+ def output_result(
13
+ logger: MCPLogger,
14
+ output_format: OutputFormat,
15
+ hook_type: str,
16
+ allowed: bool,
17
+ user_message: Optional[str] = None,
18
+ agent_message: Optional[str] = None
19
+ ) -> None:
20
+ """
21
+ Output hook result in IDE-specific format and exit with appropriate code
22
+
23
+ Args:
24
+ logger: Logger instance
25
+ output_format: IDE-specific output configuration
26
+ hook_type: "permission" or "continue"
27
+ allowed: True for allow/continue, False for deny/block
28
+ user_message: Optional message for user
29
+ agent_message: Optional message for agent/logs
30
+ """
31
+ # Format output using IDE-specific formatter
32
+ formatted_output = output_format.formatter(hook_type, allowed, user_message, agent_message)
33
+
34
+ logger.info(f"Hook output ({hook_type}, allowed={allowed}): {formatted_output}")
35
+ print(formatted_output, flush=True)
36
+
37
+ # Exit with appropriate code
38
+ exit_code = output_format.allow_exit_code if allowed else output_format.deny_exit_code
39
+ sys.exit(exit_code)
40
+
41
+
42
+ def output_error(
43
+ logger: MCPLogger,
44
+ output_format: OutputFormat,
45
+ hook_type: str,
46
+ error_message: str
47
+ ) -> None:
48
+ """
49
+ Output error and exit with error code
50
+
51
+ Args:
52
+ logger: Logger instance
53
+ output_format: IDE-specific output configuration
54
+ hook_type: "permission" or "continue"
55
+ error_message: Error message
56
+ """
57
+ logger.error(f"Hook error: {error_message}")
58
+
59
+ # Output as deny/block with error message
60
+ formatted_output = output_format.formatter(hook_type, False, error_message, error_message)
61
+ print(formatted_output, flush=True)
62
+
63
+ sys.exit(output_format.error_exit_code)
@@ -0,0 +1,136 @@
1
+ """
2
+ Shared logic for UserPromptSubmit hook
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+
7
+ from modules.logs.audit_trail import AuditTrailLogger
8
+ from modules.logs.logger import MCPLogger
9
+ from modules.redaction import redact
10
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
11
+ from modules.utils.string import truncate_at
12
+ from .output import output_result, output_error
13
+ from .types import HookConfig
14
+ from .utils import create_validator, extract_redaction_patterns, process_attachments_for_redaction, inspect_and_enforce
15
+
16
+
17
+ async def handle_prompt_submit(
18
+ logger: MCPLogger,
19
+ audit_logger: AuditTrailLogger,
20
+ stdin_input: str,
21
+ prompt_id: str,
22
+ event_id: str,
23
+ cwd: Optional[str],
24
+ config: HookConfig,
25
+ tool_name: str
26
+ ) -> None:
27
+ """
28
+ Shared handler for prompt submission hooks
29
+
30
+ Args:
31
+ logger: Logger instance
32
+ audit_logger: Audit logger instance
33
+ stdin_input: Raw JSON input
34
+ prompt_id: Prompt/conversation ID
35
+ event_id: Event/generation ID
36
+ cwd: Current working directory
37
+ config: IDE-specific hook configuration
38
+ tool_name: IDE-specific tool name (e.g., "beforeSubmitPrompt", "UserPromptSubmit")
39
+ """
40
+ session_id = get_session_id()
41
+ logger.info(
42
+ f"Prompt submit handler started (client={config.client_name}, prompt_id={prompt_id}, "
43
+ f"event_id={event_id}, cwd={cwd})")
44
+
45
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
46
+ audit_logger.set_app_uid(app_uid)
47
+
48
+ try:
49
+ try:
50
+ validator = create_validator(
51
+ required_fields={"prompt": str},
52
+ optional_fields={"attachments": list}
53
+ )
54
+ input_data = validator(stdin_input)
55
+ prompt = input_data["prompt"]
56
+ attachments = input_data.get("attachments", [])
57
+ except ValueError as e:
58
+ logger.error(f"Input validation error: {e}")
59
+ output_error(logger, config.output_format, "continue", str(e))
60
+ return
61
+
62
+ redacted_prompt = redact(prompt)
63
+
64
+ audit_logger.log_event(
65
+ "prompt_submission",
66
+ {
67
+ "server": config.server_name,
68
+ "tool": tool_name,
69
+ "params": {"prompt": truncate_at(redacted_prompt, 100), "attachments_count": len(attachments)}
70
+ },
71
+ event_id=event_id,
72
+ prompt_id=prompt_id
73
+ )
74
+
75
+ prompt_patterns = extract_redaction_patterns(redacted_prompt)
76
+
77
+ # Check for redactions in file attachments
78
+ files_with_redactions = process_attachments_for_redaction(
79
+ attachments,
80
+ logger
81
+ )
82
+
83
+ has_any_redactions = bool(prompt_patterns) or len(files_with_redactions) > 0
84
+
85
+ content_data: Dict[str, Any] = {
86
+ "prompt": redacted_prompt,
87
+ "is_redacted": has_any_redactions,
88
+ "redacted_files": files_with_redactions,
89
+ }
90
+
91
+ # Call security API and enforce decision
92
+ try:
93
+ decision = await inspect_and_enforce(
94
+ is_request=True,
95
+ session_id=session_id,
96
+ logger=logger,
97
+ audit_logger=audit_logger,
98
+ app_uid=app_uid,
99
+ event_id=event_id,
100
+ server_name=config.server_name,
101
+ tool_name=tool_name,
102
+ content_data=content_data,
103
+ prompt_id=prompt_id,
104
+ cwd=cwd,
105
+ client_name=config.client_name
106
+ )
107
+
108
+ audit_logger.log_event(
109
+ "prompt_submission_forwarded",
110
+ {
111
+ "server": config.server_name,
112
+ "tool": tool_name,
113
+ "params": {"redactions_found": has_any_redactions}
114
+ },
115
+ event_id=event_id,
116
+ prompt_id=prompt_id
117
+ )
118
+
119
+ reasons = decision.get("reasons", [])
120
+ agent_message = "Prompt submission approved: {0}".format("; ".join(
121
+ reasons)) if reasons else "Prompt submission approved by security policy"
122
+ output_result(logger, config.output_format, "continue", True,
123
+ "Prompt approved", agent_message)
124
+
125
+ except Exception as e:
126
+ # Decision enforcement failed - block
127
+ error_msg = str(e)
128
+ user_message = "Prompt blocked by security policy"
129
+ if "User blocked" in error_msg or "User denied" in error_msg:
130
+ user_message = "Prompt blocked by user"
131
+
132
+ output_result(logger, config.output_format, "continue", False, user_message, error_msg)
133
+
134
+ except Exception as e:
135
+ logger.error(f"Unexpected error in prompt submit handler: {e}", exc_info=True)
136
+ output_error(logger, config.output_format, "continue", f"Unexpected error: {str(e)}")
@@ -0,0 +1,170 @@
1
+ """
2
+ Shared logic for beforeReadFile/PreReadFile hook
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from modules.logs.audit_trail import AuditTrailLogger
8
+ from modules.logs.logger import MCPLogger
9
+ from modules.redaction import redact
10
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
11
+ from .output import output_result, output_error
12
+ from .types import HookConfig
13
+ from .utils import create_validator, process_attachments_for_redaction, inspect_and_enforce
14
+
15
+
16
+ async def handle_read_file(
17
+ logger: MCPLogger,
18
+ audit_logger: AuditTrailLogger,
19
+ stdin_input: str,
20
+ prompt_id: str,
21
+ event_id: str,
22
+ cwd: Optional[str],
23
+ config: HookConfig,
24
+ tool_name: str
25
+ ) -> None:
26
+ """
27
+ Shared handler for file read hooks
28
+
29
+ Args:
30
+ logger: Logger instance
31
+ audit_logger: Audit logger instance
32
+ stdin_input: Raw JSON input
33
+ prompt_id: Prompt/conversation ID
34
+ event_id: Event/generation ID
35
+ cwd: Current working directory
36
+ config: IDE-specific hook configuration
37
+ tool_name: IDE-specific tool name (e.g., "beforeReadFile", "PreToolUse")
38
+ """
39
+ session_id = get_session_id()
40
+ logger.info(
41
+ f"Read file handler started (client={config.client_name}, prompt_id={prompt_id}, event_id={event_id}, cwd={cwd})")
42
+
43
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
44
+ audit_logger.set_app_uid(app_uid)
45
+
46
+ try:
47
+ try:
48
+ validator = create_validator(
49
+ required_fields={"file_path": str, "content": str},
50
+ optional_fields={"attachments": list}
51
+ )
52
+ input_data = validator(stdin_input)
53
+ file_path = input_data["file_path"]
54
+ provided_content = input_data["content"]
55
+ attachments = input_data.get("attachments", [])
56
+ except ValueError as e:
57
+ logger.error(f"Input validation error: {e}")
58
+ output_error(logger, config.output_format, "permission", str(e))
59
+ return
60
+
61
+ audit_logger.log_event(
62
+ "agent_request",
63
+ {
64
+ "server": config.server_name,
65
+ "tool": tool_name,
66
+ "params": {"file_path": file_path, "attachments_count": len(attachments)}
67
+ },
68
+ event_id=event_id,
69
+ prompt_id=prompt_id
70
+ )
71
+
72
+ # Check content length - skip API if too large
73
+ if len(provided_content) > config.max_content_length:
74
+ logger.info(f"Content length ({len(provided_content)} chars) exceeds max ({config.max_content_length}) - "
75
+ f"skipping API call")
76
+
77
+ audit_logger.log_event(
78
+ "agent_request_forwarded",
79
+ {
80
+ "server": config.server_name,
81
+ "tool": tool_name,
82
+ "params": {
83
+ "file_path": file_path,
84
+ "content_length": len(provided_content),
85
+ "content_too_large": True
86
+ }
87
+ },
88
+ event_id=event_id,
89
+ prompt_id=prompt_id
90
+ )
91
+
92
+ output_result(logger, config.output_format, "permission", True)
93
+ return
94
+
95
+ # Redact the main content
96
+ redacted_content = redact(provided_content)
97
+
98
+ # Process attachments for redaction status
99
+ files_with_redactions = process_attachments_for_redaction(attachments, logger)
100
+ files_with_redactions_paths = {f["file_path"] for f in files_with_redactions}
101
+
102
+ # Build attachments info with redaction status
103
+ attachments_info = []
104
+ for attachment in attachments:
105
+ att_path = attachment.get("file_path") or attachment.get("filePath")
106
+ if att_path:
107
+ attachments_info.append({
108
+ "file_path": att_path,
109
+ "has_redactions": att_path in files_with_redactions_paths
110
+ })
111
+
112
+ logger.info(f"Processed file and {len(attachments)} attachment(s), found redactions in "
113
+ f"{len(files_with_redactions)} attachment(s)")
114
+
115
+ # Build content_data with file_path, redacted content, and attachments
116
+ content_data = {
117
+ "file_path": file_path,
118
+ "content": redacted_content,
119
+ "attachments": attachments_info
120
+ }
121
+
122
+ # Call security API and enforce decision
123
+ try:
124
+ decision = await inspect_and_enforce(
125
+ is_request=True,
126
+ session_id=session_id,
127
+ logger=logger,
128
+ audit_logger=audit_logger,
129
+ app_uid=app_uid,
130
+ event_id=event_id,
131
+ server_name=config.server_name,
132
+ tool_name=tool_name,
133
+ content_data=content_data,
134
+ prompt_id=prompt_id,
135
+ cwd=cwd,
136
+ current_files=[file_path],
137
+ client_name=config.client_name
138
+ )
139
+
140
+ audit_logger.log_event(
141
+ "agent_request_forwarded",
142
+ {
143
+ "server": config.server_name,
144
+ "tool": tool_name,
145
+ "params": {
146
+ "file_path": file_path,
147
+ "content_length": len(provided_content),
148
+ "attachments_with_redactions": len(files_with_redactions)}
149
+ },
150
+ event_id=event_id,
151
+ prompt_id=prompt_id
152
+ )
153
+
154
+ reasons = decision.get("reasons", [])
155
+ agent_message = "File read approved: " + "; ".join(
156
+ reasons) if reasons else "File read approved by security policy"
157
+ output_result(logger, config.output_format, "permission", True, "File read approved", agent_message)
158
+
159
+ except Exception as e:
160
+ # Decision enforcement failed - block
161
+ error_msg = str(e)
162
+ user_message = "File read blocked by security policy"
163
+ if "User blocked" in error_msg or "User denied" in error_msg:
164
+ user_message = "File read blocked by user"
165
+
166
+ output_result(logger, config.output_format, "permission", False, user_message, error_msg)
167
+
168
+ except Exception as e:
169
+ logger.error(f"Unexpected error in read file handler: {e}", exc_info=True)
170
+ output_error(logger, config.output_format, "permission", f"Unexpected error: {str(e)}")