cycode 3.25.2.dev1__py3-none-any.whl → 3.25.2.dev3__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/app.py +9 -10
- cycode/cli/apps/activation_manager.py +6 -6
- cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
- cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +6 -5
- cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
- cycode/cli/apps/ai_guardrails/ides/base.py +9 -11
- cycode/cli/apps/ai_guardrails/ides/claude_code.py +14 -14
- cycode/cli/apps/ai_guardrails/ides/codex.py +11 -11
- cycode/cli/apps/ai_guardrails/ides/copilot.py +7 -7
- cycode/cli/apps/ai_guardrails/ides/cursor.py +5 -5
- cycode/cli/apps/ai_guardrails/install_command.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +10 -11
- cycode/cli/apps/ai_guardrails/scan/handlers.py +15 -14
- cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +6 -6
- cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
- cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
- cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
- cycode/cli/apps/ai_guardrails/session_start_command.py +8 -8
- cycode/cli/apps/ai_guardrails/status_command.py +2 -2
- cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
- cycode/cli/apps/api/__init__.py +3 -3
- cycode/cli/apps/api/api_command.py +6 -6
- cycode/cli/apps/api/openapi_spec.py +4 -5
- cycode/cli/apps/auth/auth_common.py +4 -4
- cycode/cli/apps/configure/configure_command.py +2 -4
- cycode/cli/apps/configure/messages.py +2 -4
- cycode/cli/apps/configure/prompts.py +5 -7
- cycode/cli/apps/ignore/ignore_command.py +7 -7
- cycode/cli/apps/mcp/mcp_command.py +11 -11
- cycode/cli/apps/report/sbom/common.py +5 -5
- cycode/cli/apps/report/sbom/sbom_command.py +2 -2
- cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
- cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
- cycode/cli/apps/sca_options.py +3 -3
- cycode/cli/apps/scan/aggregation_report.py +4 -4
- cycode/cli/apps/scan/code_scanner.py +8 -7
- cycode/cli/apps/scan/commit_range_scanner.py +10 -10
- cycode/cli/apps/scan/detection_excluder.py +2 -4
- cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
- cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
- cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
- cycode/cli/apps/scan/remote_url_resolver.py +6 -8
- cycode/cli/apps/scan/repository/repository_command.py +2 -2
- cycode/cli/apps/scan/scan_command.py +2 -2
- cycode/cli/apps/scan/scan_parameters.py +1 -3
- cycode/cli/apps/scan/scan_result.py +5 -5
- cycode/cli/console.py +2 -2
- cycode/cli/consts.py +2 -0
- cycode/cli/exceptions/custom_exceptions.py +1 -1
- cycode/cli/exceptions/handle_errors.py +1 -3
- cycode/cli/exceptions/handle_scan_errors.py +1 -2
- cycode/cli/files_collector/commit_range_documents.py +14 -14
- cycode/cli/files_collector/models/in_memory_zip.py +3 -3
- cycode/cli/files_collector/repository_documents.py +3 -3
- cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
- cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
- cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
- cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
- cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
- cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
- cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
- cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
- cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
- cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
- cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
- cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
- cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
- cycode/cli/files_collector/zip_documents.py +1 -2
- cycode/cli/models.py +8 -8
- cycode/cli/printers/console_printer.py +9 -9
- cycode/cli/printers/json_printer.py +2 -2
- cycode/cli/printers/printer_base.py +3 -3
- cycode/cli/printers/rich_printer.py +2 -2
- cycode/cli/printers/tables/table.py +4 -4
- cycode/cli/printers/tables/table_models.py +2 -2
- cycode/cli/printers/tables/table_printer_base.py +2 -2
- cycode/cli/printers/text_printer.py +4 -4
- cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
- cycode/cli/printers/utils/detection_data.py +3 -3
- cycode/cli/printers/utils/sca_ossf.py +4 -4
- cycode/cli/printers/utils/sca_policy_details.py +2 -1
- cycode/cli/user_settings/config_file_manager.py +10 -10
- cycode/cli/user_settings/configuration_manager.py +6 -6
- cycode/cli/user_settings/credentials_manager.py +5 -6
- cycode/cli/utils/get_api_client.py +5 -5
- cycode/cli/utils/git_proxy.py +8 -8
- cycode/cli/utils/host_info.py +21 -38
- cycode/cli/utils/ignore_utils.py +13 -15
- cycode/cli/utils/jwt_utils.py +2 -4
- cycode/cli/utils/path_utils.py +6 -6
- cycode/cli/utils/progress_bar.py +4 -4
- cycode/cli/utils/scan_batch.py +2 -1
- cycode/cli/utils/scan_utils.py +2 -2
- cycode/cli/utils/shell_executor.py +3 -4
- cycode/cli/utils/task_timer.py +4 -4
- cycode/cli/utils/trust_store.py +0 -17
- cycode/cli/utils/url_utils.py +1 -2
- cycode/cli/utils/version_checker.py +4 -5
- cycode/cli/utils/yaml_utils.py +1 -1
- cycode/config.py +1 -2
- cycode/cyclient/ai_security_manager_client.py +17 -17
- cycode/cyclient/auth_client.py +2 -4
- cycode/cyclient/base_token_auth_client.py +2 -2
- cycode/cyclient/client_creator.py +4 -6
- cycode/cyclient/cycode_client_base.py +10 -9
- cycode/cyclient/cycode_dev_based_client.py +1 -3
- cycode/cyclient/headers.py +1 -2
- cycode/cyclient/import_sbom_client.py +3 -4
- cycode/cyclient/models.py +33 -33
- cycode/cyclient/report_client.py +1 -2
- cycode/cyclient/scan_client.py +8 -7
- cycode/logger.py +4 -4
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/METADATA +12 -19
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/RECORD +124 -124
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/WHEEL +1 -1
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/entry_points.txt +0 -0
- {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev3.dist-info}/licenses/LICENCE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Install command for AI guardrails hooks."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
@@ -29,7 +29,7 @@ def install_command(
|
|
|
29
29
|
),
|
|
30
30
|
] = DEFAULT_IDE_NAME,
|
|
31
31
|
repo_path: Annotated[
|
|
32
|
-
|
|
32
|
+
Path | None,
|
|
33
33
|
typer.Option(
|
|
34
34
|
'--repo-path',
|
|
35
35
|
help='Repository path for repo-scoped installation (defaults to current directory).',
|
|
@@ -93,7 +93,7 @@ def install_command(
|
|
|
93
93
|
raise typer.Exit(1)
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def _install_policy(scope: str, repo_path:
|
|
96
|
+
def _install_policy(scope: str, repo_path: Path | None) -> None:
|
|
97
97
|
policy_success, policy_message = create_policy_file(scope, repo_path)
|
|
98
98
|
if policy_success:
|
|
99
99
|
console.print(f'[green]✓[/] {policy_message}')
|
|
@@ -10,7 +10,6 @@ import json
|
|
|
10
10
|
import time
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Optional
|
|
14
13
|
|
|
15
14
|
from cycode.cli.apps.ai_guardrails.consts import GuardrailCellMode, PolicyMode
|
|
16
15
|
from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_SENSITIVE_PATH_GLOBS
|
|
@@ -60,7 +59,7 @@ def _default_sensitive_globs() -> list:
|
|
|
60
59
|
class GuardrailConfig:
|
|
61
60
|
payload: dict
|
|
62
61
|
fetched_at: float
|
|
63
|
-
tenant_id:
|
|
62
|
+
tenant_id: str | None = None
|
|
64
63
|
_guardrails: dict = field(init=False, repr=False)
|
|
65
64
|
|
|
66
65
|
def __post_init__(self) -> None:
|
|
@@ -77,7 +76,7 @@ class GuardrailConfig:
|
|
|
77
76
|
def _agents(self, guardrail_key: str) -> dict:
|
|
78
77
|
return (self._guardrails.get(guardrail_key) or {}).get('agents') or {}
|
|
79
78
|
|
|
80
|
-
def mode_for(self, guardrail_key: str, ide_name:
|
|
79
|
+
def mode_for(self, guardrail_key: str, ide_name: str | None) -> str:
|
|
81
80
|
"""The platform keys the cells by our --ide names, so the lookup is direct."""
|
|
82
81
|
agents = self._agents(guardrail_key)
|
|
83
82
|
return str(agents.get((ide_name or '').lower(), default_mode_for(guardrail_key))).lower()
|
|
@@ -85,19 +84,19 @@ class GuardrailConfig:
|
|
|
85
84
|
def is_off_for_every_agent(self, guardrail_key: str) -> bool:
|
|
86
85
|
return all(str(mode).lower() == GuardrailCellMode.OFF for mode in self._agents(guardrail_key).values())
|
|
87
86
|
|
|
88
|
-
def _modes_for_event(self, event_name: str, ide_name:
|
|
87
|
+
def _modes_for_event(self, event_name: str, ide_name: str | None) -> list:
|
|
89
88
|
return [
|
|
90
89
|
self.mode_for(key, ide_name)
|
|
91
90
|
for key, guardrail in self._guardrails.items()
|
|
92
91
|
if str(guardrail.get('event_type', '')).lower() == str(event_name).lower()
|
|
93
92
|
]
|
|
94
93
|
|
|
95
|
-
def is_event_off(self, event_name: str, ide_name:
|
|
94
|
+
def is_event_off(self, event_name: str, ide_name: str | None) -> bool:
|
|
96
95
|
"""Every guardrail for this event is Off - skip the scan entirely."""
|
|
97
96
|
modes = self._modes_for_event(event_name, ide_name)
|
|
98
97
|
return bool(modes) and all(mode == GuardrailCellMode.OFF for mode in modes)
|
|
99
98
|
|
|
100
|
-
def can_event_block(self, event_name: str, ide_name:
|
|
99
|
+
def can_event_block(self, event_name: str, ide_name: str | None) -> bool:
|
|
101
100
|
"""At least one guardrail for this event is in Block mode - the scan must stay synchronous."""
|
|
102
101
|
return GuardrailCellMode.BLOCK in self._modes_for_event(event_name, ide_name)
|
|
103
102
|
|
|
@@ -109,12 +108,12 @@ class GuardrailConfig:
|
|
|
109
108
|
def is_expired(self) -> bool:
|
|
110
109
|
return time.time() - self.fetched_at > self.ttl_seconds
|
|
111
110
|
|
|
112
|
-
def needs_refresh(self, tenant_id:
|
|
111
|
+
def needs_refresh(self, tenant_id: str | None) -> bool:
|
|
113
112
|
"""Expired, or fetched for another tenant (the user switched tenants since)."""
|
|
114
113
|
return self.is_expired() or self.tenant_id != tenant_id
|
|
115
114
|
|
|
116
115
|
|
|
117
|
-
def apply_platform_config(policy: dict, config:
|
|
116
|
+
def apply_platform_config(policy: dict, config: GuardrailConfig | None, ide_name: str | None) -> None:
|
|
118
117
|
"""Overlay the platform-owned enforcement config onto the local knobs-only policy.
|
|
119
118
|
|
|
120
119
|
The platform is the only mode source: no cache (cold start) means the built-in defaults -
|
|
@@ -149,7 +148,7 @@ def apply_platform_config(policy: dict, config: Optional[GuardrailConfig], ide_n
|
|
|
149
148
|
mcp['server_action'] = action(BlockReason.UNAUTHORIZED_MCP_SERVER)
|
|
150
149
|
|
|
151
150
|
|
|
152
|
-
def save_guardrail_config(payload: dict, tenant_id:
|
|
151
|
+
def save_guardrail_config(payload: dict, tenant_id: str | None) -> None:
|
|
153
152
|
"""Persist a fetched resolved config; a failed write just leaves the previous cache in place."""
|
|
154
153
|
path = get_config_cache_path()
|
|
155
154
|
content = {'fetched_at': time.time(), 'tenant_id': tenant_id, 'payload': payload}
|
|
@@ -160,7 +159,7 @@ def save_guardrail_config(payload: dict, tenant_id: Optional[str]) -> None:
|
|
|
160
159
|
logger.debug('Failed to save guardrail config cache', exc_info=e)
|
|
161
160
|
|
|
162
161
|
|
|
163
|
-
def load_guardrail_config() ->
|
|
162
|
+
def load_guardrail_config() -> GuardrailConfig | None:
|
|
164
163
|
"""The cached platform config, or None when it is absent or corrupt (quarantined)."""
|
|
165
164
|
path = get_config_cache_path()
|
|
166
165
|
if not path.exists():
|
|
@@ -171,7 +170,7 @@ def load_guardrail_config() -> Optional[GuardrailConfig]:
|
|
|
171
170
|
content = json.load(file)
|
|
172
171
|
payload = content['payload']
|
|
173
172
|
if not isinstance(payload, dict):
|
|
174
|
-
raise
|
|
173
|
+
raise TypeError('payload is not an object')
|
|
175
174
|
return GuardrailConfig(
|
|
176
175
|
payload=payload, fetched_at=float(content['fetched_at']), tenant_id=content.get('tenant_id')
|
|
177
176
|
)
|
|
@@ -11,8 +11,9 @@ touching any handler in this module.
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from dataclasses import dataclass
|
|
15
|
-
from typing import TYPE_CHECKING, Any,
|
|
16
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
16
17
|
|
|
17
18
|
import typer
|
|
18
19
|
|
|
@@ -48,15 +49,15 @@ HandlerFn = Callable[[typer.Context, AIHookPayload, dict], HookDecision]
|
|
|
48
49
|
class ScanOutcome(NamedTuple):
|
|
49
50
|
"""What one guardrail scan came back with; the verdict is the server's, which applied the tenant's floors."""
|
|
50
51
|
|
|
51
|
-
violation_summary:
|
|
52
|
-
scan_id:
|
|
53
|
-
verdict:
|
|
52
|
+
violation_summary: str | None = None
|
|
53
|
+
scan_id: str | None = None
|
|
54
|
+
verdict: GuardrailsMode | None = None
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
NO_SCAN = ScanOutcome()
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def _parse_verdict(verdict:
|
|
60
|
+
def _parse_verdict(verdict: str | None) -> GuardrailsMode | None:
|
|
60
61
|
"""The server spells the verdict "Block"/"Report" and omits it when the scan found nothing to decide on."""
|
|
61
62
|
if not verdict:
|
|
62
63
|
return None
|
|
@@ -105,7 +106,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
|
|
|
105
106
|
)
|
|
106
107
|
block_reason = BlockReason.SCAN_FAILURE
|
|
107
108
|
error_message = str(e)
|
|
108
|
-
raise
|
|
109
|
+
raise
|
|
109
110
|
finally:
|
|
110
111
|
ai_client.create_event(
|
|
111
112
|
payload,
|
|
@@ -192,7 +193,7 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
|
|
|
192
193
|
)
|
|
193
194
|
block_reason = BlockReason.SCAN_FAILURE
|
|
194
195
|
error_message = str(e)
|
|
195
|
-
raise
|
|
196
|
+
raise
|
|
196
197
|
finally:
|
|
197
198
|
ai_client.create_event(
|
|
198
199
|
payload,
|
|
@@ -236,7 +237,7 @@ def _handle_arg_scan(
|
|
|
236
237
|
policy: dict,
|
|
237
238
|
feature: _ArgScanFeature,
|
|
238
239
|
scan_text: str,
|
|
239
|
-
pre_scan_finding:
|
|
240
|
+
pre_scan_finding: _PreScanFinding | None = None,
|
|
240
241
|
) -> HookDecision:
|
|
241
242
|
"""Shared scan + decision flow for MCP_EXECUTION and COMMAND_EXEC events.
|
|
242
243
|
|
|
@@ -297,7 +298,7 @@ def _handle_arg_scan(
|
|
|
297
298
|
)
|
|
298
299
|
block_reason = BlockReason.SCAN_FAILURE
|
|
299
300
|
error_message = str(e)
|
|
300
|
-
raise
|
|
301
|
+
raise
|
|
301
302
|
finally:
|
|
302
303
|
ai_client.create_event(
|
|
303
304
|
payload,
|
|
@@ -309,7 +310,7 @@ def _handle_arg_scan(
|
|
|
309
310
|
)
|
|
310
311
|
|
|
311
312
|
|
|
312
|
-
def _check_mcp_server_authorization(payload: AIHookPayload, policy: dict) ->
|
|
313
|
+
def _check_mcp_server_authorization(payload: AIHookPayload, policy: dict) -> _PreScanFinding | None:
|
|
313
314
|
"""Fails open: no server name or no cached statuses lets the call through."""
|
|
314
315
|
mcp_config = get_policy_value(policy, 'mcp', default={})
|
|
315
316
|
if not get_policy_value(mcp_config, 'check_server', default=False):
|
|
@@ -366,7 +367,7 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
|
|
|
366
367
|
)
|
|
367
368
|
|
|
368
369
|
|
|
369
|
-
def get_handler_for_event(event_type: str) ->
|
|
370
|
+
def get_handler_for_event(event_type: str) -> HandlerFn | None:
|
|
370
371
|
"""Look up the handler for a canonical event type."""
|
|
371
372
|
handlers: dict[str, HandlerFn] = {
|
|
372
373
|
AiHookEventType.PROMPT.value: handle_before_submit_prompt,
|
|
@@ -383,10 +384,10 @@ def get_effective_mode(feature_config: dict, action_key: str = 'action') -> Guar
|
|
|
383
384
|
|
|
384
385
|
|
|
385
386
|
def should_detach_scan(
|
|
386
|
-
config:
|
|
387
|
+
config: 'GuardrailConfig | None',
|
|
387
388
|
policy: dict,
|
|
388
389
|
event_name: str,
|
|
389
|
-
ide_name:
|
|
390
|
+
ide_name: str | None,
|
|
390
391
|
) -> bool:
|
|
391
392
|
"""Whether this event's scan is safe to run detached.
|
|
392
393
|
|
|
@@ -403,7 +404,7 @@ def should_detach_scan(
|
|
|
403
404
|
|
|
404
405
|
def build_ai_guardrails_scan_parameters(
|
|
405
406
|
ctx: typer.Context,
|
|
406
|
-
paths:
|
|
407
|
+
paths: tuple[str, ...] | None,
|
|
407
408
|
payload: AIHookPayload,
|
|
408
409
|
event_type: AiHookEventType,
|
|
409
410
|
) -> dict:
|
|
@@ -7,7 +7,7 @@ import json
|
|
|
7
7
|
import time
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
from marshmallow import EXCLUDE, Schema, fields, post_load
|
|
13
13
|
|
|
@@ -34,7 +34,7 @@ def get_mcp_server_statuses_cache_path() -> Path:
|
|
|
34
34
|
return Path.home() / CYCODE_CONFIGURATION_DIRECTORY / MCP_SERVER_STATUSES_FILE_NAME
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def is_enforced(status:
|
|
37
|
+
def is_enforced(status: McpServerAuthorizationStatus | None) -> bool:
|
|
38
38
|
"""``status`` is None when the platform never saw the server."""
|
|
39
39
|
return status == McpServerAuthorizationStatus.UNAUTHORIZED
|
|
40
40
|
|
|
@@ -43,7 +43,7 @@ def is_enforced(status: Optional[McpServerAuthorizationStatus]) -> bool:
|
|
|
43
43
|
class McpServerStatuses:
|
|
44
44
|
servers: list[McpServerStatus]
|
|
45
45
|
fetched_at: float
|
|
46
|
-
tenant_id:
|
|
46
|
+
tenant_id: str | None = None
|
|
47
47
|
ttl_seconds: float = DEFAULT_TTL_SECONDS
|
|
48
48
|
_by_alias: dict = field(init=False, repr=False)
|
|
49
49
|
|
|
@@ -58,7 +58,7 @@ class McpServerStatuses:
|
|
|
58
58
|
if known is None or _RESTRICTIVENESS[server.status] > _RESTRICTIVENESS[known]:
|
|
59
59
|
self._by_alias[key] = server.status
|
|
60
60
|
|
|
61
|
-
def status_of(self, alias: str) ->
|
|
61
|
+
def status_of(self, alias: str) -> McpServerAuthorizationStatus | None:
|
|
62
62
|
return self._by_alias.get(alias.lower())
|
|
63
63
|
|
|
64
64
|
|
|
@@ -78,7 +78,7 @@ class McpServerStatusesSchema(Schema):
|
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
def save_mcp_server_statuses(
|
|
81
|
-
servers: list[McpServerStatus], tenant_id:
|
|
81
|
+
servers: list[McpServerStatus], tenant_id: str | None, ttl_seconds: float = DEFAULT_TTL_SECONDS
|
|
82
82
|
) -> None:
|
|
83
83
|
path = get_mcp_server_statuses_cache_path()
|
|
84
84
|
statuses = McpServerStatuses(servers=servers, fetched_at=time.time(), tenant_id=tenant_id, ttl_seconds=ttl_seconds)
|
|
@@ -89,7 +89,7 @@ def save_mcp_server_statuses(
|
|
|
89
89
|
logger.debug('Failed to save MCP server statuses cache', exc_info=e)
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def load_mcp_server_statuses() ->
|
|
92
|
+
def load_mcp_server_statuses() -> McpServerStatuses | None:
|
|
93
93
|
path = get_mcp_server_statuses_cache_path()
|
|
94
94
|
if not path.exists():
|
|
95
95
|
return None
|
|
@@ -7,7 +7,6 @@ respective IDE class.
|
|
|
7
7
|
|
|
8
8
|
import uuid
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
|
-
from typing import Optional
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@dataclass
|
|
@@ -15,9 +14,9 @@ class AIHookPayload:
|
|
|
15
14
|
"""Unified payload that normalizes field names across IDEs."""
|
|
16
15
|
|
|
17
16
|
# Event identification
|
|
18
|
-
event_name:
|
|
19
|
-
conversation_id:
|
|
20
|
-
generation_id:
|
|
17
|
+
event_name: str | None = None # Canonical event type from AiHookEventType
|
|
18
|
+
conversation_id: str | None = None
|
|
19
|
+
generation_id: str | None = None
|
|
21
20
|
|
|
22
21
|
# Minted here rather than by the server: the guardrail scan and the hook event are reported in two
|
|
23
22
|
# separate requests, and both have to name the same event. A generation id can't stand in for it - the
|
|
@@ -25,16 +24,16 @@ class AIHookPayload:
|
|
|
25
24
|
hook_event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
26
25
|
|
|
27
26
|
# User and IDE information
|
|
28
|
-
ide_user_email:
|
|
29
|
-
model:
|
|
30
|
-
ide_provider:
|
|
31
|
-
ide_version:
|
|
27
|
+
ide_user_email: str | None = None
|
|
28
|
+
model: str | None = None
|
|
29
|
+
ide_provider: str | None = None # Matches IDE.name (e.g. 'cursor', 'claude-code')
|
|
30
|
+
ide_version: str | None = None
|
|
32
31
|
|
|
33
|
-
source:
|
|
32
|
+
source: str | None = None
|
|
34
33
|
|
|
35
34
|
# Event-specific data
|
|
36
|
-
prompt:
|
|
37
|
-
file_path:
|
|
38
|
-
mcp_server_name:
|
|
39
|
-
mcp_tool_name:
|
|
40
|
-
mcp_arguments:
|
|
35
|
+
prompt: str | None = None # PROMPT events
|
|
36
|
+
file_path: str | None = None # FILE_READ events
|
|
37
|
+
mcp_server_name: str | None = None # MCP_EXECUTION events
|
|
38
|
+
mcp_tool_name: str | None = None
|
|
39
|
+
mcp_arguments: dict | None = None
|
|
@@ -15,7 +15,7 @@ import json
|
|
|
15
15
|
import os
|
|
16
16
|
import sys
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
18
|
+
from typing import Any
|
|
19
19
|
|
|
20
20
|
import yaml
|
|
21
21
|
|
|
@@ -71,7 +71,7 @@ def deep_merge(base: dict, override: dict) -> dict:
|
|
|
71
71
|
return result
|
|
72
72
|
|
|
73
73
|
|
|
74
|
-
def load_yaml_file(path: Path) ->
|
|
74
|
+
def load_yaml_file(path: Path) -> dict | None:
|
|
75
75
|
"""Load a YAML or JSON config file."""
|
|
76
76
|
if not path.exists():
|
|
77
77
|
return None
|
|
@@ -101,7 +101,7 @@ def get_policy_value(policy: dict, *keys: str, default: Any = None) -> Any:
|
|
|
101
101
|
return current
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def load_policy(workspace_root:
|
|
104
|
+
def load_policy(workspace_root: str | None = None) -> dict:
|
|
105
105
|
"""
|
|
106
106
|
Load policy by merging configs in order of precedence.
|
|
107
107
|
|
|
@@ -7,7 +7,7 @@ The handlers in ``handlers.py`` are agent-agnostic (they return
|
|
|
7
7
|
``HookDecision``); ``IDE.build_hook_response`` is the per-IDE translation step.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from typing import Annotated
|
|
10
|
+
from typing import Annotated
|
|
11
11
|
from uuid import uuid4
|
|
12
12
|
|
|
13
13
|
import click
|
|
@@ -46,9 +46,9 @@ def _get_auth_error_message(error: Exception) -> str:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _deny_for_event(
|
|
49
|
-
event_name:
|
|
49
|
+
event_name: str | AiHookEventType | None,
|
|
50
50
|
user_message: str,
|
|
51
|
-
agent_message:
|
|
51
|
+
agent_message: str | None = None,
|
|
52
52
|
) -> HookDecision:
|
|
53
53
|
"""Build a deny decision matched to ``event_name``'s response shape.
|
|
54
54
|
|
|
@@ -62,7 +62,7 @@ def _deny_for_event(
|
|
|
62
62
|
return HookDecision.deny(target, user_message, agent_message)
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def _should_skip_payload(ide_integration: IDE, payload:
|
|
65
|
+
def _should_skip_payload(ide_integration: IDE, payload: dict | None) -> bool:
|
|
66
66
|
"""Fast exits that never scan: empty/foreign/synthetic payloads all answer a plain allow."""
|
|
67
67
|
if not payload:
|
|
68
68
|
logger.debug('Empty or invalid JSON payload received')
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
import time
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, Annotated
|
|
8
|
+
from typing import TYPE_CHECKING, Annotated
|
|
9
9
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
@@ -56,14 +56,14 @@ def _session_context_digest(report: dict) -> str:
|
|
|
56
56
|
return hashlib.sha256(canonical.encode('utf-8')).hexdigest()
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def _report_ttl_seconds(config:
|
|
59
|
+
def _report_ttl_seconds(config: GuardrailConfig | None) -> float:
|
|
60
60
|
"""The response carries the MCP server statuses, so the guardrail's TTL applies while it is enabled."""
|
|
61
61
|
if config is None or config.is_off_for_every_agent(BlockReason.UNAUTHORIZED_MCP_SERVER):
|
|
62
62
|
return _SESSION_CONTEXT_TTL_SECONDS
|
|
63
63
|
return config.ttl_seconds
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def _should_skip_report(digest: str, tenant_id:
|
|
66
|
+
def _should_skip_report(digest: str, tenant_id: str | None, ttl_seconds: float) -> bool:
|
|
67
67
|
"""Skip when the same payload was already sent for this tenant and the TTL hasn't expired."""
|
|
68
68
|
try:
|
|
69
69
|
cache = json.loads(_session_context_cache_path().read_text(encoding='utf-8'))
|
|
@@ -77,7 +77,7 @@ def _should_skip_report(digest: str, tenant_id: Optional[str], ttl_seconds: floa
|
|
|
77
77
|
return False
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def _save_report_cache(digest: str, tenant_id:
|
|
80
|
+
def _save_report_cache(digest: str, tenant_id: str | None) -> None:
|
|
81
81
|
try:
|
|
82
82
|
cache_path = _session_context_cache_path()
|
|
83
83
|
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -90,9 +90,9 @@ def _save_report_cache(digest: str, tenant_id: Optional[str]) -> None:
|
|
|
90
90
|
|
|
91
91
|
def _report_session_context(
|
|
92
92
|
ai_client: 'AISecurityManagerClient',
|
|
93
|
-
user_email:
|
|
94
|
-
tenant_id:
|
|
95
|
-
config:
|
|
93
|
+
user_email: str | None,
|
|
94
|
+
tenant_id: str | None,
|
|
95
|
+
config: GuardrailConfig | None,
|
|
96
96
|
) -> None:
|
|
97
97
|
"""Report the device + cross-IDE session context to the AI security manager. Never raises.
|
|
98
98
|
|
|
@@ -195,7 +195,7 @@ def session_start_command(
|
|
|
195
195
|
_report_session_context(ai_client, session_payload.ide_user_email, auth_info.tenant_id, config)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
-
def _sync_guardrail_config(ai_client: 'AISecurityManagerClient', tenant_id:
|
|
198
|
+
def _sync_guardrail_config(ai_client: 'AISecurityManagerClient', tenant_id: str | None) -> GuardrailConfig | None:
|
|
199
199
|
"""Refresh the guardrail config cache when it is expired or belongs to another tenant.
|
|
200
200
|
|
|
201
201
|
Every step here swallows its own failures - a broken cache or an unreachable platform
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
8
8
|
from rich.table import Table
|
|
@@ -30,7 +30,7 @@ def status_command(
|
|
|
30
30
|
),
|
|
31
31
|
] = DEFAULT_IDE_NAME,
|
|
32
32
|
repo_path: Annotated[
|
|
33
|
-
|
|
33
|
+
Path | None,
|
|
34
34
|
typer.Option(
|
|
35
35
|
'--repo-path',
|
|
36
36
|
help='Repository path for repo-scoped status (defaults to current directory).',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Uninstall command for AI guardrails hooks."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
|
|
@@ -28,7 +28,7 @@ def uninstall_command(
|
|
|
28
28
|
),
|
|
29
29
|
] = DEFAULT_IDE_NAME,
|
|
30
30
|
repo_path: Annotated[
|
|
31
|
-
|
|
31
|
+
Path | None,
|
|
32
32
|
typer.Option(
|
|
33
33
|
'--repo-path',
|
|
34
34
|
help='Repository path for repo-scoped uninstallation (defaults to current directory).',
|
cycode/cli/apps/api/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ The spec is fetched lazily — only when the user invokes `cycode platform ...`
|
|
|
5
5
|
and cached locally for 24 hours.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
11
|
|
|
@@ -32,7 +32,7 @@ class PlatformGroup(click.Group):
|
|
|
32
32
|
super().__init__(*args, **kwargs)
|
|
33
33
|
self._loaded: bool = False
|
|
34
34
|
|
|
35
|
-
def _ensure_loaded(self, ctx:
|
|
35
|
+
def _ensure_loaded(self, ctx: click.Context | None) -> None:
|
|
36
36
|
if self._loaded:
|
|
37
37
|
return
|
|
38
38
|
self._loaded = True # set first to avoid re-entrancy on errors
|
|
@@ -59,7 +59,7 @@ class PlatformGroup(click.Group):
|
|
|
59
59
|
self._ensure_loaded(ctx)
|
|
60
60
|
return super().list_commands(ctx)
|
|
61
61
|
|
|
62
|
-
def get_command(self, ctx: click.Context, cmd_name: str) ->
|
|
62
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
63
63
|
self._ensure_loaded(ctx)
|
|
64
64
|
return super().get_command(ctx, cmd_name)
|
|
65
65
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
@@ -60,7 +60,7 @@ def _path_to_command_name(path: str, common_prefix: str, has_path_params: bool)
|
|
|
60
60
|
/v4/violations/count (prefix=/v4/violations) -> count
|
|
61
61
|
"""
|
|
62
62
|
# Strip common prefix
|
|
63
|
-
relative = path
|
|
63
|
+
relative = path.removeprefix(common_prefix)
|
|
64
64
|
relative = relative.strip('/')
|
|
65
65
|
|
|
66
66
|
# Remove path parameter segments and empty parts
|
|
@@ -91,8 +91,8 @@ def _make_api_request(
|
|
|
91
91
|
method: str,
|
|
92
92
|
path_params: dict[str, str],
|
|
93
93
|
query_params: dict[str, Any],
|
|
94
|
-
client_id:
|
|
95
|
-
client_secret:
|
|
94
|
+
client_id: str | None = None,
|
|
95
|
+
client_secret: str | None = None,
|
|
96
96
|
) -> dict:
|
|
97
97
|
"""Execute an API request using the CLI's standard auth client."""
|
|
98
98
|
from urllib.parse import quote
|
|
@@ -115,8 +115,8 @@ def _make_api_request(
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
def build_api_command_groups(
|
|
118
|
-
client_id:
|
|
119
|
-
client_secret:
|
|
118
|
+
client_id: str | None = None,
|
|
119
|
+
client_secret: str | None = None,
|
|
120
120
|
) -> list[tuple[click.Group, str]]:
|
|
121
121
|
"""Build Click command groups from the OpenAPI spec.
|
|
122
122
|
|
|
@@ -4,7 +4,6 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY
|
|
10
9
|
from cycode.cli.user_settings.credentials_manager import CredentialsManager
|
|
@@ -20,7 +19,7 @@ _CACHE_TTL_SECONDS = int(os.getenv('CYCODE_SPEC_CACHE_TTL', str(24 * 60 * 60)))
|
|
|
20
19
|
_OPENAPI_SPEC_PATH = '/v4/api-docs/cycode-api-swagger.json'
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
def get_openapi_spec(client_id:
|
|
22
|
+
def get_openapi_spec(client_id: str | None = None, client_secret: str | None = None) -> dict:
|
|
24
23
|
"""Get the OpenAPI spec, using cache if fresh, otherwise fetching from API.
|
|
25
24
|
|
|
26
25
|
The spec is only fetched when the user actually invokes `cycode platform ...`.
|
|
@@ -45,7 +44,7 @@ def get_openapi_spec(client_id: Optional[str] = None, client_secret: Optional[st
|
|
|
45
44
|
return _fetch_and_cache_spec(client_id, client_secret)
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
def _load_cached_spec() ->
|
|
47
|
+
def _load_cached_spec() -> dict | None:
|
|
49
48
|
"""Load spec from local cache if it exists and is fresh."""
|
|
50
49
|
if not _CACHE_FILE.exists():
|
|
51
50
|
return None
|
|
@@ -64,7 +63,7 @@ def _load_cached_spec() -> Optional[dict]:
|
|
|
64
63
|
return None
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
def resolve_credentials(client_id:
|
|
66
|
+
def resolve_credentials(client_id: str | None = None, client_secret: str | None = None) -> tuple[str, str]:
|
|
68
67
|
"""Resolve credentials from args or the CLI's standard credential chain."""
|
|
69
68
|
if not client_id or not client_secret:
|
|
70
69
|
credentials_manager = CredentialsManager()
|
|
@@ -81,7 +80,7 @@ def resolve_credentials(client_id: Optional[str] = None, client_secret: Optional
|
|
|
81
80
|
return client_id, client_secret
|
|
82
81
|
|
|
83
82
|
|
|
84
|
-
def _fetch_and_cache_spec(client_id:
|
|
83
|
+
def _fetch_and_cache_spec(client_id: str | None = None, client_secret: str | None = None) -> dict:
|
|
85
84
|
"""Fetch OpenAPI spec from API and cache to disk.
|
|
86
85
|
|
|
87
86
|
Uses CycodeTokenBasedClient for auth and retries. The spec is served from the app URL,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
from cycode.cli.apps.auth.models import AuthInfo
|
|
4
4
|
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError, RequestHttpError
|
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from typer import Context
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def get_authorization_info(ctx: 'Context') ->
|
|
14
|
+
def get_authorization_info(ctx: 'Context') -> AuthInfo | None:
|
|
15
15
|
printer = ctx.obj.get('console_printer')
|
|
16
16
|
|
|
17
17
|
client_id = ctx.obj.get('client_id')
|
|
@@ -50,8 +50,8 @@ def get_authorization_info(ctx: 'Context') -> Optional[AuthInfo]:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _try_oidc_authorization(
|
|
53
|
-
ctx: 'Context', printer: any, client_id:
|
|
54
|
-
) ->
|
|
53
|
+
ctx: 'Context', printer: any, client_id: str | None, id_token: str | None
|
|
54
|
+
) -> AuthInfo | None:
|
|
55
55
|
if not client_id or not id_token:
|
|
56
56
|
return None
|
|
57
57
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
from cycode.cli.apps.configure.consts import CONFIGURATION_MANAGER, CREDENTIALS_MANAGER
|
|
4
2
|
from cycode.cli.apps.configure.messages import (
|
|
5
3
|
get_credentials_environment_variables_override_warning,
|
|
@@ -18,8 +16,8 @@ from cycode.cli.console import console
|
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
def _should_update_value(
|
|
21
|
-
old_value:
|
|
22
|
-
new_value:
|
|
19
|
+
old_value: str | None,
|
|
20
|
+
new_value: str | None,
|
|
23
21
|
) -> bool:
|
|
24
22
|
if not new_value:
|
|
25
23
|
return False
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
from cycode.cli.apps.configure.consts import (
|
|
4
2
|
CONFIGURATION_MANAGER,
|
|
5
3
|
CREDENTIALS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE,
|
|
@@ -19,7 +17,7 @@ def get_credentials_update_result_message() -> str:
|
|
|
19
17
|
return CREDENTIALS_UPDATED_SUCCESSFULLY_MESSAGE.format(filename=CREDENTIALS_MANAGER.get_filename())
|
|
20
18
|
|
|
21
19
|
|
|
22
|
-
def get_credentials_environment_variables_override_warning() ->
|
|
20
|
+
def get_credentials_environment_variables_override_warning() -> str | None:
|
|
23
21
|
if _are_credentials_exist_in_environment_variables():
|
|
24
22
|
return CREDENTIALS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE
|
|
25
23
|
|
|
@@ -38,7 +36,7 @@ def get_urls_update_result_message() -> str:
|
|
|
38
36
|
)
|
|
39
37
|
|
|
40
38
|
|
|
41
|
-
def get_urls_environment_variables_override_warning() ->
|
|
39
|
+
def get_urls_environment_variables_override_warning() -> str | None:
|
|
42
40
|
if _are_urls_exist_in_environment_variables():
|
|
43
41
|
return URLS_ARE_SET_IN_ENVIRONMENT_VARIABLES_MESSAGE
|
|
44
42
|
|