cycode 3.15.3.dev7__py3-none-any.whl → 3.15.4.dev1__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.
- cycode/__init__.py +1 -1
- cycode/cli/apps/ai_guardrails/command_utils.py +2 -45
- cycode/cli/apps/ai_guardrails/consts.py +3 -135
- cycode/cli/apps/ai_guardrails/hooks_manager.py +51 -138
- cycode/cli/apps/ai_guardrails/ides/__init__.py +44 -0
- cycode/cli/apps/ai_guardrails/ides/base.py +156 -0
- cycode/cli/apps/ai_guardrails/ides/claude_code.py +389 -0
- cycode/cli/apps/ai_guardrails/ides/cursor.py +119 -0
- cycode/cli/apps/ai_guardrails/install_command.py +14 -23
- cycode/cli/apps/ai_guardrails/scan/handlers.py +46 -89
- cycode/cli/apps/ai_guardrails/scan/payload.py +14 -255
- cycode/cli/apps/ai_guardrails/scan/scan_command.py +60 -48
- cycode/cli/apps/ai_guardrails/scan/types.py +8 -30
- cycode/cli/apps/ai_guardrails/session_start_command.py +14 -78
- cycode/cli/apps/ai_guardrails/status_command.py +13 -16
- cycode/cli/apps/ai_guardrails/uninstall_command.py +12 -22
- {cycode-3.15.3.dev7.dist-info → cycode-3.15.4.dev1.dist-info}/METADATA +36 -1
- {cycode-3.15.3.dev7.dist-info → cycode-3.15.4.dev1.dist-info}/RECORD +21 -20
- cycode/cli/apps/ai_guardrails/scan/claude_config.py +0 -159
- cycode/cli/apps/ai_guardrails/scan/cursor_config.py +0 -36
- cycode/cli/apps/ai_guardrails/scan/response_builders.py +0 -135
- {cycode-3.15.3.dev7.dist-info → cycode-3.15.4.dev1.dist-info}/WHEEL +0 -0
- {cycode-3.15.3.dev7.dist-info → cycode-3.15.4.dev1.dist-info}/entry_points.txt +0 -0
- {cycode-3.15.3.dev7.dist-info → cycode-3.15.4.dev1.dist-info}/licenses/LICENCE +0 -0
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""Reader for ~/.claude.json configuration file.
|
|
2
|
-
|
|
3
|
-
Extracts user email from the Claude Code global config file
|
|
4
|
-
for use in AI guardrails scan enrichment.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
from cycode.logger import get_logger
|
|
12
|
-
|
|
13
|
-
logger = get_logger('AI Guardrails Claude Config')
|
|
14
|
-
|
|
15
|
-
_CLAUDE_CONFIG_PATH = Path.home() / '.claude.json'
|
|
16
|
-
_CLAUDE_SETTINGS_PATH = Path.home() / '.claude' / 'settings.json'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]:
|
|
20
|
-
"""Load and parse ~/.claude.json.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
config_path: Override path for testing. Defaults to ~/.claude.json.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Parsed dict or None if file is missing or invalid.
|
|
27
|
-
"""
|
|
28
|
-
path = config_path or _CLAUDE_CONFIG_PATH
|
|
29
|
-
if not path.exists():
|
|
30
|
-
logger.debug('Claude config file not found', extra={'path': str(path)})
|
|
31
|
-
return None
|
|
32
|
-
try:
|
|
33
|
-
content = path.read_text(encoding='utf-8')
|
|
34
|
-
return json.loads(content)
|
|
35
|
-
except Exception as e:
|
|
36
|
-
logger.debug('Failed to load Claude config file', exc_info=e)
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_user_email(config: dict) -> Optional[str]:
|
|
41
|
-
"""Extract user email from Claude config.
|
|
42
|
-
|
|
43
|
-
Reads oauthAccount.emailAddress from the config dict.
|
|
44
|
-
"""
|
|
45
|
-
return config.get('oauthAccount', {}).get('emailAddress')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_mcp_servers(config: dict) -> Optional[dict]:
|
|
49
|
-
"""Extract MCP servers from Claude config.
|
|
50
|
-
|
|
51
|
-
Reads mcpServers from the config dict.
|
|
52
|
-
"""
|
|
53
|
-
return config.get('mcpServers')
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def load_claude_settings(settings_path: Optional[Path] = None) -> Optional[dict]:
|
|
57
|
-
"""Load and parse ~/.claude/settings.json.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
settings_path: Override path for testing. Defaults to ~/.claude/settings.json.
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
Parsed dict or None if file is missing or invalid.
|
|
64
|
-
"""
|
|
65
|
-
path = settings_path or _CLAUDE_SETTINGS_PATH
|
|
66
|
-
if not path.exists():
|
|
67
|
-
logger.debug('Claude settings file not found', extra={'path': str(path)})
|
|
68
|
-
return None
|
|
69
|
-
try:
|
|
70
|
-
content = path.read_text(encoding='utf-8')
|
|
71
|
-
return json.loads(content)
|
|
72
|
-
except Exception as e:
|
|
73
|
-
logger.debug('Failed to load Claude settings file', exc_info=e)
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _resolve_marketplace_path(marketplace: dict) -> Optional[Path]:
|
|
78
|
-
"""
|
|
79
|
-
Resolve filesystem path for a directory-type marketplace.
|
|
80
|
-
"""
|
|
81
|
-
source = marketplace.get('source', {})
|
|
82
|
-
if source.get('source') != 'directory':
|
|
83
|
-
return None
|
|
84
|
-
raw = source.get('path')
|
|
85
|
-
if not raw:
|
|
86
|
-
return None
|
|
87
|
-
path = Path(raw)
|
|
88
|
-
return path if path.is_dir() else None
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _load_plugin_json_file(plugin_path: Path, relative_path: str) -> Optional[dict]:
|
|
92
|
-
"""Load and parse a JSON file inside a plugin directory.
|
|
93
|
-
|
|
94
|
-
Returns None if the file is missing, unreadable, or has invalid JSON.
|
|
95
|
-
"""
|
|
96
|
-
target = plugin_path / relative_path
|
|
97
|
-
if not target.exists():
|
|
98
|
-
return None
|
|
99
|
-
try:
|
|
100
|
-
return json.loads(target.read_text(encoding='utf-8'))
|
|
101
|
-
except Exception as e:
|
|
102
|
-
logger.debug('Failed to load plugin file', extra={'path': str(target)}, exc_info=e)
|
|
103
|
-
return None
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def resolve_plugins(settings: dict) -> tuple[dict, dict]:
|
|
107
|
-
"""Resolve enabled plugins to their MCP servers and metadata.
|
|
108
|
-
|
|
109
|
-
Walks enabledPlugins from claude settings, resolves each plugin's 'marketplace' directory
|
|
110
|
-
via the 'extraKnownMarketplaces' field, and reads:
|
|
111
|
-
- <path>/.mcp.json for MCP servers (merged into a flat dict)
|
|
112
|
-
- <path>/.claude-plugin/plugin.json for metadata (name, version, description)
|
|
113
|
-
|
|
114
|
-
Args:
|
|
115
|
-
settings: Parsed ~/.claude/settings.json dict.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Tuple of (merged_mcp_servers, enriched_plugins):
|
|
119
|
-
- merged_mcp_servers: {server_name: server_config, ...}
|
|
120
|
-
- enriched_plugins: {plugin_key: {"enabled": True, "name": ..., ...}, ...}
|
|
121
|
-
"""
|
|
122
|
-
enabled = settings.get('enabledPlugins') or {}
|
|
123
|
-
marketplaces = settings.get('extraKnownMarketplaces') or {}
|
|
124
|
-
merged_mcp: dict = {}
|
|
125
|
-
enriched: dict = {}
|
|
126
|
-
|
|
127
|
-
for plugin_key, is_enabled in enabled.items():
|
|
128
|
-
if not is_enabled:
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
entry: dict = {'enabled': True}
|
|
132
|
-
enriched[plugin_key] = entry
|
|
133
|
-
|
|
134
|
-
if '@' not in plugin_key:
|
|
135
|
-
continue
|
|
136
|
-
|
|
137
|
-
_plugin_name, marketplace_name = plugin_key.split('@', 1)
|
|
138
|
-
marketplace = marketplaces.get(marketplace_name)
|
|
139
|
-
if not marketplace:
|
|
140
|
-
continue
|
|
141
|
-
|
|
142
|
-
plugin_path = _resolve_marketplace_path(marketplace)
|
|
143
|
-
if plugin_path is None:
|
|
144
|
-
continue
|
|
145
|
-
|
|
146
|
-
metadata = _load_plugin_json_file(plugin_path, '.claude-plugin/plugin.json') or {}
|
|
147
|
-
for field in ('name', 'version', 'description'):
|
|
148
|
-
if field in metadata:
|
|
149
|
-
entry[field] = metadata[field]
|
|
150
|
-
|
|
151
|
-
mcp_config = _load_plugin_json_file(plugin_path, '.mcp.json') or {}
|
|
152
|
-
plugin_server_names = []
|
|
153
|
-
for server_name, server_cfg in (mcp_config.get('mcpServers') or {}).items():
|
|
154
|
-
merged_mcp[server_name] = server_cfg
|
|
155
|
-
plugin_server_names.append(server_name)
|
|
156
|
-
if plugin_server_names:
|
|
157
|
-
entry['mcp_server_names'] = plugin_server_names
|
|
158
|
-
|
|
159
|
-
return merged_mcp, enriched
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""Reader for ~/.cursor/mcp.json configuration file.
|
|
2
|
-
|
|
3
|
-
Extracts MCP server definitions from the Cursor global config file
|
|
4
|
-
for use in AI guardrails session-context reporting.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
from cycode.logger import get_logger
|
|
12
|
-
|
|
13
|
-
logger = get_logger('AI Guardrails Cursor Config')
|
|
14
|
-
|
|
15
|
-
_CURSOR_MCP_CONFIG_PATH = Path.home() / '.cursor' / 'mcp.json'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def load_cursor_config(config_path: Optional[Path] = None) -> Optional[dict]:
|
|
19
|
-
"""Load and parse ~/.cursor/mcp.json.
|
|
20
|
-
|
|
21
|
-
Args:
|
|
22
|
-
config_path: Override path for testing. Defaults to ~/.cursor/mcp.json.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
Parsed dict or None if file is missing or invalid.
|
|
26
|
-
"""
|
|
27
|
-
path = config_path or _CURSOR_MCP_CONFIG_PATH
|
|
28
|
-
if not path.exists():
|
|
29
|
-
logger.debug('Cursor MCP config file not found', extra={'path': str(path)})
|
|
30
|
-
return None
|
|
31
|
-
try:
|
|
32
|
-
content = path.read_text(encoding='utf-8')
|
|
33
|
-
return json.loads(content)
|
|
34
|
-
except Exception as e:
|
|
35
|
-
logger.debug('Failed to load Cursor MCP config file', exc_info=e)
|
|
36
|
-
return None
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Response builders for different AI IDE hooks.
|
|
3
|
-
|
|
4
|
-
Each IDE has its own response format for hooks. This module provides
|
|
5
|
-
an abstract interface and concrete implementations for each supported IDE.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from abc import ABC, abstractmethod
|
|
9
|
-
|
|
10
|
-
from cycode.cli.apps.ai_guardrails.consts import AIIDEType
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class IDEResponseBuilder(ABC):
|
|
14
|
-
"""Abstract base class for IDE-specific response builders."""
|
|
15
|
-
|
|
16
|
-
@abstractmethod
|
|
17
|
-
def allow_permission(self) -> dict:
|
|
18
|
-
"""Build response to allow file read or MCP execution."""
|
|
19
|
-
|
|
20
|
-
@abstractmethod
|
|
21
|
-
def deny_permission(self, user_message: str, agent_message: str) -> dict:
|
|
22
|
-
"""Build response to deny file read or MCP execution."""
|
|
23
|
-
|
|
24
|
-
@abstractmethod
|
|
25
|
-
def ask_permission(self, user_message: str, agent_message: str) -> dict:
|
|
26
|
-
"""Build response to ask user for permission (warn mode)."""
|
|
27
|
-
|
|
28
|
-
@abstractmethod
|
|
29
|
-
def allow_prompt(self) -> dict:
|
|
30
|
-
"""Build response to allow prompt submission."""
|
|
31
|
-
|
|
32
|
-
@abstractmethod
|
|
33
|
-
def deny_prompt(self, user_message: str) -> dict:
|
|
34
|
-
"""Build response to deny prompt submission."""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class CursorResponseBuilder(IDEResponseBuilder):
|
|
38
|
-
"""Response builder for Cursor IDE hooks.
|
|
39
|
-
|
|
40
|
-
Cursor hook response formats:
|
|
41
|
-
- beforeSubmitPrompt: {"continue": bool, "user_message": str}
|
|
42
|
-
- beforeReadFile: {"permission": str, "user_message": str, "agent_message": str}
|
|
43
|
-
- beforeMCPExecution: {"permission": str, "user_message": str, "agent_message": str}
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def allow_permission(self) -> dict:
|
|
47
|
-
"""Allow file read or MCP execution."""
|
|
48
|
-
return {'permission': 'allow'}
|
|
49
|
-
|
|
50
|
-
def deny_permission(self, user_message: str, agent_message: str) -> dict:
|
|
51
|
-
"""Deny file read or MCP execution."""
|
|
52
|
-
return {'permission': 'deny', 'user_message': user_message, 'agent_message': agent_message}
|
|
53
|
-
|
|
54
|
-
def ask_permission(self, user_message: str, agent_message: str) -> dict:
|
|
55
|
-
"""Ask user for permission (warn mode)."""
|
|
56
|
-
return {'permission': 'ask', 'user_message': user_message, 'agent_message': agent_message}
|
|
57
|
-
|
|
58
|
-
def allow_prompt(self) -> dict:
|
|
59
|
-
"""Allow prompt submission."""
|
|
60
|
-
return {'continue': True}
|
|
61
|
-
|
|
62
|
-
def deny_prompt(self, user_message: str) -> dict:
|
|
63
|
-
"""Deny prompt submission."""
|
|
64
|
-
return {'continue': False, 'user_message': user_message}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class ClaudeCodeResponseBuilder(IDEResponseBuilder):
|
|
68
|
-
"""Response builder for Claude Code IDE hooks.
|
|
69
|
-
|
|
70
|
-
Claude Code hook response formats:
|
|
71
|
-
- UserPromptSubmit: {} for allow, {"decision": "block", "reason": str} for deny
|
|
72
|
-
- PreToolUse: hookSpecificOutput with permissionDecision (allow/deny/ask)
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
def allow_permission(self) -> dict:
|
|
76
|
-
"""Allow file read or MCP execution."""
|
|
77
|
-
return {
|
|
78
|
-
'hookSpecificOutput': {
|
|
79
|
-
'hookEventName': 'PreToolUse',
|
|
80
|
-
'permissionDecision': 'allow',
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
def deny_permission(self, user_message: str, agent_message: str) -> dict:
|
|
85
|
-
"""Deny file read or MCP execution."""
|
|
86
|
-
return {
|
|
87
|
-
'hookSpecificOutput': {
|
|
88
|
-
'hookEventName': 'PreToolUse',
|
|
89
|
-
'permissionDecision': 'deny',
|
|
90
|
-
'permissionDecisionReason': user_message,
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
def ask_permission(self, user_message: str, agent_message: str) -> dict:
|
|
95
|
-
"""Ask user for permission (warn mode)."""
|
|
96
|
-
return {
|
|
97
|
-
'hookSpecificOutput': {
|
|
98
|
-
'hookEventName': 'PreToolUse',
|
|
99
|
-
'permissionDecision': 'ask',
|
|
100
|
-
'permissionDecisionReason': user_message,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
def allow_prompt(self) -> dict:
|
|
105
|
-
"""Allow prompt submission (empty response means allow)."""
|
|
106
|
-
return {}
|
|
107
|
-
|
|
108
|
-
def deny_prompt(self, user_message: str) -> dict:
|
|
109
|
-
"""Deny prompt submission."""
|
|
110
|
-
return {'decision': 'block', 'reason': user_message}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Registry of response builders by IDE type
|
|
114
|
-
_RESPONSE_BUILDERS: dict[str, IDEResponseBuilder] = {
|
|
115
|
-
AIIDEType.CURSOR: CursorResponseBuilder(),
|
|
116
|
-
AIIDEType.CLAUDE_CODE: ClaudeCodeResponseBuilder(),
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def get_response_builder(ide: str = AIIDEType.CURSOR.value) -> IDEResponseBuilder:
|
|
121
|
-
"""Get the response builder for a specific IDE.
|
|
122
|
-
|
|
123
|
-
Args:
|
|
124
|
-
ide: The IDE name (e.g., 'cursor', 'claude-code') or AIIDEType enum
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
IDEResponseBuilder instance for the specified IDE
|
|
128
|
-
|
|
129
|
-
Raises:
|
|
130
|
-
ValueError: If the IDE is not supported
|
|
131
|
-
"""
|
|
132
|
-
builder = _RESPONSE_BUILDERS.get(ide.lower())
|
|
133
|
-
if not builder:
|
|
134
|
-
raise ValueError(f'Unsupported IDE: {ide}. Supported IDEs: {list(_RESPONSE_BUILDERS.keys())}')
|
|
135
|
-
return builder
|
|
File without changes
|
|
File without changes
|
|
File without changes
|