cycode 3.15.3.dev8__py3-none-any.whl → 3.15.4.dev2__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 +123 -152
- cycode/cli/apps/ai_guardrails/ides/__init__.py +45 -0
- cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +73 -0
- cycode/cli/apps/ai_guardrails/ides/base.py +176 -0
- cycode/cli/apps/ai_guardrails/ides/claude_code.py +369 -0
- cycode/cli/apps/ai_guardrails/ides/codex.py +310 -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 +102 -101
- 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/cli/utils/jwt_utils.py +8 -0
- {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/METADATA +3 -1
- {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/RECORD +24 -21
- 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.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/WHEEL +0 -0
- {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/entry_points.txt +0 -0
- {cycode-3.15.3.dev8.dist-info → cycode-3.15.4.dev2.dist-info}/licenses/LICENCE +0 -0
|
@@ -7,9 +7,9 @@ from typing import Annotated, Optional
|
|
|
7
7
|
import typer
|
|
8
8
|
from rich.table import Table
|
|
9
9
|
|
|
10
|
-
from cycode.cli.apps.ai_guardrails.command_utils import console,
|
|
11
|
-
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
|
|
10
|
+
from cycode.cli.apps.ai_guardrails.command_utils import console, validate_scope
|
|
12
11
|
from cycode.cli.apps.ai_guardrails.hooks_manager import get_hooks_status
|
|
12
|
+
from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, IDES, resolve_ides
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def status_command(
|
|
@@ -26,9 +26,9 @@ def status_command(
|
|
|
26
26
|
str,
|
|
27
27
|
typer.Option(
|
|
28
28
|
'--ide',
|
|
29
|
-
help='IDE to check status for (
|
|
29
|
+
help=f'IDE to check status for ({", ".join(IDES)}, or "all").',
|
|
30
30
|
),
|
|
31
|
-
] =
|
|
31
|
+
] = DEFAULT_IDE_NAME,
|
|
32
32
|
repo_path: Annotated[
|
|
33
33
|
Optional[Path],
|
|
34
34
|
typer.Option(
|
|
@@ -43,32 +43,30 @@ def status_command(
|
|
|
43
43
|
) -> None:
|
|
44
44
|
"""Show AI guardrails hook installation status.
|
|
45
45
|
|
|
46
|
-
Displays the current status of Cycode AI guardrails hooks for the specified IDE.
|
|
47
|
-
|
|
48
46
|
Examples:
|
|
49
47
|
cycode ai-guardrails status # Show both user and repo status
|
|
50
48
|
cycode ai-guardrails status --scope user # Show only user-level status
|
|
51
49
|
cycode ai-guardrails status --scope repo # Show only repo-level status
|
|
52
|
-
cycode ai-guardrails status --ide
|
|
53
|
-
cycode ai-guardrails status --ide all # Check
|
|
50
|
+
cycode ai-guardrails status --ide claude-code
|
|
51
|
+
cycode ai-guardrails status --ide all # Check every supported IDE
|
|
54
52
|
"""
|
|
55
|
-
# Validate inputs (status allows 'all' scope)
|
|
56
53
|
validate_scope(scope, allowed_scopes=('user', 'repo', 'all'))
|
|
57
54
|
if repo_path is None:
|
|
58
55
|
repo_path = Path(os.getcwd())
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
ides_to_check: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
|
|
56
|
+
ides_to_check = resolve_ides(ide)
|
|
62
57
|
|
|
63
58
|
scopes_to_check = ['user', 'repo'] if scope == 'all' else [scope]
|
|
64
59
|
|
|
65
60
|
for current_ide in ides_to_check:
|
|
66
|
-
ide_name = IDE_CONFIGS[current_ide].name
|
|
67
61
|
console.print()
|
|
68
|
-
console.print(f'[bold cyan]═══ {
|
|
62
|
+
console.print(f'[bold cyan]═══ {current_ide.display_name} ═══[/]')
|
|
69
63
|
|
|
70
64
|
for check_scope in scopes_to_check:
|
|
71
|
-
status = get_hooks_status(
|
|
65
|
+
status = get_hooks_status(
|
|
66
|
+
current_ide,
|
|
67
|
+
check_scope,
|
|
68
|
+
repo_path if check_scope == 'repo' else None,
|
|
69
|
+
)
|
|
72
70
|
|
|
73
71
|
console.print()
|
|
74
72
|
console.print(f'[bold]{check_scope.upper()} SCOPE[/]')
|
|
@@ -83,7 +81,6 @@ def status_command(
|
|
|
83
81
|
else:
|
|
84
82
|
console.print('[yellow]○ Cycode AI guardrails: NOT INSTALLED[/]')
|
|
85
83
|
|
|
86
|
-
# Show hook details
|
|
87
84
|
table = Table(show_header=True, header_style='bold')
|
|
88
85
|
table.add_column('Hook Event')
|
|
89
86
|
table.add_column('Cycode Enabled')
|
|
@@ -5,14 +5,9 @@ from typing import Annotated, Optional
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
8
|
-
from cycode.cli.apps.ai_guardrails.command_utils import
|
|
9
|
-
console,
|
|
10
|
-
resolve_repo_path,
|
|
11
|
-
validate_and_parse_ide,
|
|
12
|
-
validate_scope,
|
|
13
|
-
)
|
|
14
|
-
from cycode.cli.apps.ai_guardrails.consts import IDE_CONFIGS, AIIDEType
|
|
8
|
+
from cycode.cli.apps.ai_guardrails.command_utils import console, resolve_repo_path, validate_scope
|
|
15
9
|
from cycode.cli.apps.ai_guardrails.hooks_manager import uninstall_hooks
|
|
10
|
+
from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, IDES, resolve_ides
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
def uninstall_command(
|
|
@@ -29,9 +24,9 @@ def uninstall_command(
|
|
|
29
24
|
str,
|
|
30
25
|
typer.Option(
|
|
31
26
|
'--ide',
|
|
32
|
-
help='IDE to uninstall hooks from (
|
|
27
|
+
help=f'IDE to uninstall hooks from ({", ".join(IDES)}, or "all").',
|
|
33
28
|
),
|
|
34
|
-
] =
|
|
29
|
+
] = DEFAULT_IDE_NAME,
|
|
35
30
|
repo_path: Annotated[
|
|
36
31
|
Optional[Path],
|
|
37
32
|
typer.Option(
|
|
@@ -46,32 +41,27 @@ def uninstall_command(
|
|
|
46
41
|
) -> None:
|
|
47
42
|
"""Remove AI guardrails hooks from supported IDEs.
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
Removes Cycode hooks from the IDE's hooks configuration. Other hooks
|
|
45
|
+
(if any) are preserved.
|
|
51
46
|
|
|
52
47
|
Examples:
|
|
53
48
|
cycode ai-guardrails uninstall # Remove user-level hooks
|
|
54
49
|
cycode ai-guardrails uninstall --scope repo # Remove repo-level hooks
|
|
55
|
-
cycode ai-guardrails uninstall --ide
|
|
56
|
-
cycode ai-guardrails uninstall --ide all # Uninstall from
|
|
50
|
+
cycode ai-guardrails uninstall --ide claude-code # Uninstall from a specific IDE
|
|
51
|
+
cycode ai-guardrails uninstall --ide all # Uninstall from every supported IDE
|
|
57
52
|
"""
|
|
58
|
-
# Validate inputs
|
|
59
53
|
validate_scope(scope)
|
|
60
54
|
repo_path = resolve_repo_path(scope, repo_path)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
ides_to_uninstall: list[AIIDEType] = list(AIIDEType) if ide_type is None else [ide_type]
|
|
55
|
+
ides_to_uninstall = resolve_ides(ide)
|
|
64
56
|
|
|
65
57
|
results: list[tuple[str, bool, str]] = []
|
|
66
58
|
for current_ide in ides_to_uninstall:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
results.append((ide_name, success, message))
|
|
59
|
+
success, message = uninstall_hooks(current_ide, scope, repo_path)
|
|
60
|
+
results.append((current_ide.display_name, success, message))
|
|
70
61
|
|
|
71
|
-
# Report results for each IDE
|
|
72
62
|
any_success = False
|
|
73
63
|
all_success = True
|
|
74
|
-
for
|
|
64
|
+
for _name, success, message in results:
|
|
75
65
|
if success:
|
|
76
66
|
console.print(f'[green]✓[/] {message}')
|
|
77
67
|
any_success = True
|
cycode/cli/utils/jwt_utils.py
CHANGED
|
@@ -5,6 +5,14 @@ import jwt
|
|
|
5
5
|
_JWT_PAYLOAD_POSSIBLE_USER_ID_FIELD_NAMES = ('userId', 'internalId', 'token-user-id')
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def decode_jwt_unverified(token: str) -> Optional[dict]:
|
|
9
|
+
"""Return JWT claims without signature verification, or None if the token is unreadable."""
|
|
10
|
+
try:
|
|
11
|
+
return jwt.decode(token, options={'verify_signature': False})
|
|
12
|
+
except jwt.PyJWTError:
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
def get_user_and_tenant_ids_from_access_token(access_token: str) -> tuple[Optional[str], Optional[str]]:
|
|
9
17
|
payload = jwt.decode(access_token, options={'verify_signature': False})
|
|
10
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cycode
|
|
3
|
-
Version: 3.15.
|
|
3
|
+
Version: 3.15.4.dev2
|
|
4
4
|
Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENCE
|
|
@@ -34,6 +34,8 @@ Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
|
34
34
|
Requires-Dist: requests (>=2.32.4,<3.0)
|
|
35
35
|
Requires-Dist: rich (>=13.9.4,<14)
|
|
36
36
|
Requires-Dist: tenacity (>=9.0.0,<9.1.0)
|
|
37
|
+
Requires-Dist: tomli (>=2.0.0,<3.0.0) ; python_version < "3.11"
|
|
38
|
+
Requires-Dist: tomli-w (>=1.0.0,<2.0.0)
|
|
37
39
|
Requires-Dist: typer (>=0.15.3,<0.16.0)
|
|
38
40
|
Requires-Dist: urllib3 (>=2.4.0,<3.0.0)
|
|
39
41
|
Project-URL: Repository, https://github.com/cycodehq/cycode-cli
|
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
cycode/__init__.py,sha256=
|
|
1
|
+
cycode/__init__.py,sha256=QDmVY8Xl94_IN6IhGe3VfDW0t23u1d5S_iQ9f1k08Ww,115
|
|
2
2
|
cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
|
|
3
3
|
cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
cycode/cli/app.py,sha256=7ReEcVkRX9IaQ2I7jAj7Sl9smbtvxiuK8-9bitMEQik,7491
|
|
5
5
|
cycode/cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
cycode/cli/apps/activation_manager.py,sha256=Hz9PDJFB-ZmYi4HSG8iYC-fR8j5v25VuUU-l95Otsdk,1678
|
|
7
7
|
cycode/cli/apps/ai_guardrails/__init__.py,sha256=NsqB1Ca83BIjJMcDSt6suec6Ed0iNnacC0gBqkuuTtI,1367
|
|
8
|
-
cycode/cli/apps/ai_guardrails/command_utils.py,sha256=
|
|
9
|
-
cycode/cli/apps/ai_guardrails/consts.py,sha256=
|
|
10
|
-
cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=
|
|
11
|
-
cycode/cli/apps/ai_guardrails/
|
|
8
|
+
cycode/cli/apps/ai_guardrails/command_utils.py,sha256=NVwd0-2RGRKIqhsQ-4LNDR1D0gVm_o7n-z5LxG2bqAo,800
|
|
9
|
+
cycode/cli/apps/ai_guardrails/consts.py,sha256=Js2QtSNYG9Kt0eo3vepRd5TFciCeJHEC9NN18zqKvlE,620
|
|
10
|
+
cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=c8okVel9KjeXWm5QTnvTraWsTwzrUTqqKd6C80C34Y4,9003
|
|
11
|
+
cycode/cli/apps/ai_guardrails/ides/__init__.py,sha256=JMXbQlq-7q1429w3nQq9Z4FZ8K0zdiUK6zCbilmD3JU,1689
|
|
12
|
+
cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py,sha256=_wJfdUHeCJGHQIOdoXQLuMJ4YGaOq_aLjY0ZUyzpsxU,2747
|
|
13
|
+
cycode/cli/apps/ai_guardrails/ides/base.py,sha256=tFWjkuTKBn-4IZUFXHThwKctB6CWOxnG9q9Yr9fscJA,6594
|
|
14
|
+
cycode/cli/apps/ai_guardrails/ides/claude_code.py,sha256=2Rpc22lKGdAmZOCIQtIOTh34g-J_AMUMCDUJocJHzag,13552
|
|
15
|
+
cycode/cli/apps/ai_guardrails/ides/codex.py,sha256=Ep2rNsULM4FzZnej0YXb9KyKBRGIj7qPs-eC78MZd3k,11939
|
|
16
|
+
cycode/cli/apps/ai_guardrails/ides/cursor.py,sha256=_u-DS4Pdvu_UiNh-W5i2ViHPV0IDlDvFrpRnisL3ks8,5235
|
|
17
|
+
cycode/cli/apps/ai_guardrails/install_command.py,sha256=vGZSIvHHVMS9_zhV_6lEhxqtmr5H6uykSq4AS5nxQYw,4278
|
|
12
18
|
cycode/cli/apps/ai_guardrails/scan/__init__.py,sha256=qJc82XiQGiAuc1sYY8Ij_A-qXpxgLPuayQq8xWlouMA,48
|
|
13
|
-
cycode/cli/apps/ai_guardrails/scan/claude_config.py,sha256=2hVuPHfT-9_kgf5yCNgN522IcporEZvJEyYTLaaae2c,5195
|
|
14
19
|
cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=drAslw6vW3kxmbCs2qPCUbUPR7PJouT2lsXtu5sD-lQ,1094
|
|
15
|
-
cycode/cli/apps/ai_guardrails/scan/
|
|
16
|
-
cycode/cli/apps/ai_guardrails/scan/
|
|
17
|
-
cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=WGvniNa0PuqgGoeNdpXGAYK6aJqOiaB38xNUYaL2CWk,10458
|
|
20
|
+
cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=pf5PrUIVnGLEEE6QKPny9at5V6Ms5u2IEtFP72hKgqA,15523
|
|
21
|
+
cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=pvT3UUqNMvdK3EVzzPjy4JMlOrF-WgxZ3fHN2AtN5eA,1126
|
|
18
22
|
cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=39s8hnxgjny1l6XAO59wsRcAlpW-LG00GUnO0PfqvuY,2566
|
|
19
|
-
cycode/cli/apps/ai_guardrails/scan/
|
|
20
|
-
cycode/cli/apps/ai_guardrails/scan/
|
|
21
|
-
cycode/cli/apps/ai_guardrails/scan/types.py,sha256=H25MKJhAXmp7Mz1YeCIRmAY1Zg5GSpgBq8G1TEI9PFk,1868
|
|
23
|
+
cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=-Gl7cHELF1wlwLGno6ZVukFtK6NI6Z3-dTpIOsz8ors,6079
|
|
24
|
+
cycode/cli/apps/ai_guardrails/scan/types.py,sha256=lDttkYFBfOkdMEEaRbq1IT2QTK0R-7Ht3T8vZdyB3b4,1038
|
|
22
25
|
cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=KVfX-NrcM-QW4quLtoNqfmz4GF0FlDs-TkqUOu1hAWM,2057
|
|
23
|
-
cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=
|
|
24
|
-
cycode/cli/apps/ai_guardrails/status_command.py,sha256=
|
|
25
|
-
cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=
|
|
26
|
+
cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=z-yClXkV-SAxh5E8zKGIyo_qbhkpujoTgnx-lUDGS0I,3279
|
|
27
|
+
cycode/cli/apps/ai_guardrails/status_command.py,sha256=Uqss68TEPCYPXpLix6Bh-4J3g-khxWsAqlIGYH5x4bQ,3203
|
|
28
|
+
cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=dOmePfZmlHAPy2zEJM1yMtSuDqvzDwtqgmLKYK-T9PI,2698
|
|
26
29
|
cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ6VNqG4aEFBAdY,553
|
|
27
30
|
cycode/cli/apps/ai_remediation/ai_remediation_command.py,sha256=u1EdebaKCEmzv9fXmnIN0xDSLcCmGyjueYKvYfLOj_8,1549
|
|
28
31
|
cycode/cli/apps/ai_remediation/apply_fix.py,sha256=9zgqiqF9HBQXi7Oz9ZIiANIAuKAMTji1PlNncCEOf5Q,817
|
|
@@ -170,7 +173,7 @@ cycode/cli/utils/enum_utils.py,sha256=h_VTCfJ-0hnhwDsEznmx56rJrCb5FQ8u6PrI6p8MP3
|
|
|
170
173
|
cycode/cli/utils/get_api_client.py,sha256=wwHabfVCDbFjcIwOn5Raho8MEPiOAgkHlGUEfXKpl8U,3542
|
|
171
174
|
cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE,2911
|
|
172
175
|
cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
|
|
173
|
-
cycode/cli/utils/jwt_utils.py,sha256=
|
|
176
|
+
cycode/cli/utils/jwt_utils.py,sha256=EGI-0CKhCGY8hIcZ9b9diq9hqtOUf8Ha8ukeVJIf974,818
|
|
174
177
|
cycode/cli/utils/path_utils.py,sha256=U5te1unzhs9pnU5d9BWExgFWElHQkgKvFxKiOF-lp-w,3245
|
|
175
178
|
cycode/cli/utils/progress_bar.py,sha256=bKBWHHdZsVkdDdWMJLfgLGR0cBYeB44P_DpRM8pvWqU,9528
|
|
176
179
|
cycode/cli/utils/scan_batch.py,sha256=5xKGVDVqoRxdKhuZkK11x4QrNqKmU20Q83E_fy8Nndk,5188
|
|
@@ -204,8 +207,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
|
|
|
204
207
|
cycode/cyclient/scan_client.py,sha256=6TK5FQkfrvV7PHqRnUzEn1PBNd2oPYVamvIixcUfe3c,16755
|
|
205
208
|
cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
|
|
206
209
|
cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
|
|
207
|
-
cycode-3.15.
|
|
208
|
-
cycode-3.15.
|
|
209
|
-
cycode-3.15.
|
|
210
|
-
cycode-3.15.
|
|
211
|
-
cycode-3.15.
|
|
210
|
+
cycode-3.15.4.dev2.dist-info/METADATA,sha256=xiTfy56jgKUKJCRHNswSmoi0vEizW1tCcM1FUQazfp0,89206
|
|
211
|
+
cycode-3.15.4.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
212
|
+
cycode-3.15.4.dev2.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
|
|
213
|
+
cycode-3.15.4.dev2.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
|
|
214
|
+
cycode-3.15.4.dev2.dist-info/RECORD,,
|
|
@@ -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
|