cycode 3.25.2.dev1__py3-none-any.whl → 3.25.2.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.
Files changed (123) hide show
  1. cycode/__init__.py +1 -1
  2. cycode/cli/app.py +9 -10
  3. cycode/cli/apps/activation_manager.py +6 -6
  4. cycode/cli/apps/ai_guardrails/command_utils.py +1 -2
  5. cycode/cli/apps/ai_guardrails/hooks_manager.py +6 -7
  6. cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +6 -5
  7. cycode/cli/apps/ai_guardrails/ides/_skill_utils.py +1 -2
  8. cycode/cli/apps/ai_guardrails/ides/base.py +9 -11
  9. cycode/cli/apps/ai_guardrails/ides/claude_code.py +14 -14
  10. cycode/cli/apps/ai_guardrails/ides/codex.py +11 -11
  11. cycode/cli/apps/ai_guardrails/ides/copilot.py +7 -7
  12. cycode/cli/apps/ai_guardrails/ides/cursor.py +5 -5
  13. cycode/cli/apps/ai_guardrails/install_command.py +3 -3
  14. cycode/cli/apps/ai_guardrails/scan/guardrail_config.py +10 -11
  15. cycode/cli/apps/ai_guardrails/scan/handlers.py +15 -14
  16. cycode/cli/apps/ai_guardrails/scan/mcp_server_status.py +6 -6
  17. cycode/cli/apps/ai_guardrails/scan/payload.py +13 -14
  18. cycode/cli/apps/ai_guardrails/scan/policy.py +3 -3
  19. cycode/cli/apps/ai_guardrails/scan/scan_command.py +4 -4
  20. cycode/cli/apps/ai_guardrails/session_start_command.py +8 -8
  21. cycode/cli/apps/ai_guardrails/status_command.py +2 -2
  22. cycode/cli/apps/ai_guardrails/uninstall_command.py +2 -2
  23. cycode/cli/apps/api/__init__.py +3 -3
  24. cycode/cli/apps/api/api_command.py +6 -6
  25. cycode/cli/apps/api/openapi_spec.py +4 -5
  26. cycode/cli/apps/auth/auth_common.py +4 -4
  27. cycode/cli/apps/configure/configure_command.py +2 -4
  28. cycode/cli/apps/configure/messages.py +2 -4
  29. cycode/cli/apps/configure/prompts.py +5 -7
  30. cycode/cli/apps/ignore/ignore_command.py +7 -7
  31. cycode/cli/apps/mcp/mcp_command.py +11 -11
  32. cycode/cli/apps/report/sbom/common.py +5 -5
  33. cycode/cli/apps/report/sbom/sbom_command.py +2 -2
  34. cycode/cli/apps/report/sbom/sbom_report_file.py +1 -2
  35. cycode/cli/apps/report_import/sbom/sbom_command.py +3 -3
  36. cycode/cli/apps/sca_options.py +3 -3
  37. cycode/cli/apps/scan/aggregation_report.py +4 -4
  38. cycode/cli/apps/scan/code_scanner.py +8 -7
  39. cycode/cli/apps/scan/commit_range_scanner.py +10 -10
  40. cycode/cli/apps/scan/detection_excluder.py +2 -4
  41. cycode/cli/apps/scan/pre_commit/pre_commit_command.py +3 -3
  42. cycode/cli/apps/scan/pre_push/pre_push_command.py +3 -3
  43. cycode/cli/apps/scan/pre_receive/pre_receive_command.py +2 -2
  44. cycode/cli/apps/scan/remote_url_resolver.py +6 -8
  45. cycode/cli/apps/scan/repository/repository_command.py +2 -2
  46. cycode/cli/apps/scan/scan_command.py +2 -2
  47. cycode/cli/apps/scan/scan_parameters.py +1 -3
  48. cycode/cli/apps/scan/scan_result.py +5 -5
  49. cycode/cli/console.py +2 -2
  50. cycode/cli/exceptions/custom_exceptions.py +1 -1
  51. cycode/cli/exceptions/handle_errors.py +1 -3
  52. cycode/cli/exceptions/handle_scan_errors.py +1 -2
  53. cycode/cli/files_collector/commit_range_documents.py +14 -14
  54. cycode/cli/files_collector/models/in_memory_zip.py +3 -3
  55. cycode/cli/files_collector/repository_documents.py +3 -3
  56. cycode/cli/files_collector/sca/base_restore_dependencies.py +7 -8
  57. cycode/cli/files_collector/sca/go/restore_go_dependencies.py +1 -2
  58. cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +2 -3
  59. cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +3 -4
  60. cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +3 -4
  61. cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +1 -2
  62. cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +2 -3
  63. cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +2 -3
  64. cycode/cli/files_collector/sca/npm/workspace/coverage.py +5 -5
  65. cycode/cli/files_collector/sca/npm/workspace/files.py +2 -3
  66. cycode/cli/files_collector/sca/npm/workspace/globs.py +1 -2
  67. cycode/cli/files_collector/sca/npm/workspace/resolvers.py +14 -16
  68. cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +1 -2
  69. cycode/cli/files_collector/sca/python/restore_pip_dependencies.py +2 -3
  70. cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +1 -2
  71. cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +2 -3
  72. cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +2 -3
  73. cycode/cli/files_collector/sca/sca_file_collector.py +5 -5
  74. cycode/cli/files_collector/zip_documents.py +1 -2
  75. cycode/cli/models.py +8 -8
  76. cycode/cli/printers/console_printer.py +9 -9
  77. cycode/cli/printers/json_printer.py +2 -2
  78. cycode/cli/printers/printer_base.py +3 -3
  79. cycode/cli/printers/rich_printer.py +2 -2
  80. cycode/cli/printers/tables/table.py +4 -4
  81. cycode/cli/printers/tables/table_models.py +2 -2
  82. cycode/cli/printers/tables/table_printer_base.py +2 -2
  83. cycode/cli/printers/text_printer.py +4 -4
  84. cycode/cli/printers/utils/code_snippet_syntax.py +1 -1
  85. cycode/cli/printers/utils/detection_data.py +3 -3
  86. cycode/cli/printers/utils/sca_ossf.py +4 -4
  87. cycode/cli/printers/utils/sca_policy_details.py +2 -1
  88. cycode/cli/user_settings/config_file_manager.py +10 -10
  89. cycode/cli/user_settings/configuration_manager.py +6 -6
  90. cycode/cli/user_settings/credentials_manager.py +5 -6
  91. cycode/cli/utils/get_api_client.py +5 -5
  92. cycode/cli/utils/git_proxy.py +8 -8
  93. cycode/cli/utils/host_info.py +21 -38
  94. cycode/cli/utils/ignore_utils.py +13 -15
  95. cycode/cli/utils/jwt_utils.py +2 -4
  96. cycode/cli/utils/path_utils.py +6 -6
  97. cycode/cli/utils/progress_bar.py +4 -4
  98. cycode/cli/utils/scan_batch.py +2 -1
  99. cycode/cli/utils/scan_utils.py +2 -2
  100. cycode/cli/utils/shell_executor.py +3 -4
  101. cycode/cli/utils/task_timer.py +4 -4
  102. cycode/cli/utils/trust_store.py +0 -17
  103. cycode/cli/utils/url_utils.py +1 -2
  104. cycode/cli/utils/version_checker.py +4 -5
  105. cycode/cli/utils/yaml_utils.py +1 -1
  106. cycode/config.py +1 -2
  107. cycode/cyclient/ai_security_manager_client.py +17 -17
  108. cycode/cyclient/auth_client.py +2 -4
  109. cycode/cyclient/base_token_auth_client.py +2 -2
  110. cycode/cyclient/client_creator.py +4 -6
  111. cycode/cyclient/cycode_client_base.py +10 -9
  112. cycode/cyclient/cycode_dev_based_client.py +1 -3
  113. cycode/cyclient/headers.py +1 -2
  114. cycode/cyclient/import_sbom_client.py +3 -4
  115. cycode/cyclient/models.py +33 -33
  116. cycode/cyclient/report_client.py +1 -2
  117. cycode/cyclient/scan_client.py +8 -7
  118. cycode/logger.py +4 -4
  119. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev2.dist-info}/METADATA +12 -19
  120. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev2.dist-info}/RECORD +123 -123
  121. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev2.dist-info}/WHEEL +1 -1
  122. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev2.dist-info}/entry_points.txt +0 -0
  123. {cycode-3.25.2.dev1.dist-info → cycode-3.25.2.dev2.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, Optional
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
- Optional[Path],
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: Optional[Path]) -> None:
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: Optional[str] = None
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: Optional[str]) -> str:
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: Optional[str]) -> list:
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: Optional[str]) -> bool:
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: Optional[str]) -> bool:
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: Optional[str]) -> bool:
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: Optional[GuardrailConfig], ide_name: Optional[str]) -> None:
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: Optional[str]) -> None:
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() -> Optional[GuardrailConfig]:
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 ValueError('payload is not an object')
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, Callable, NamedTuple, Optional
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: Optional[str] = None
52
- scan_id: Optional[str] = None
53
- verdict: Optional[GuardrailsMode] = None
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: Optional[str]) -> Optional[GuardrailsMode]:
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 e
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 e
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: Optional[_PreScanFinding] = None,
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 e
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) -> Optional[_PreScanFinding]:
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) -> Optional[HandlerFn]:
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: Optional['GuardrailConfig'],
387
+ config: 'GuardrailConfig | None',
387
388
  policy: dict,
388
389
  event_name: str,
389
- ide_name: Optional[str],
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: Optional[tuple[str, ...]],
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, Optional
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: Optional[McpServerAuthorizationStatus]) -> bool:
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: Optional[str] = None
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) -> Optional[McpServerAuthorizationStatus]:
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: Optional[str], ttl_seconds: float = DEFAULT_TTL_SECONDS
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() -> Optional[McpServerStatuses]:
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: Optional[str] = None # Canonical event type from AiHookEventType
19
- conversation_id: Optional[str] = None
20
- generation_id: Optional[str] = None
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: Optional[str] = None
29
- model: Optional[str] = None
30
- ide_provider: Optional[str] = None # Matches IDE.name (e.g. 'cursor', 'claude-code')
31
- ide_version: Optional[str] = None
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: Optional[str] = None
32
+ source: str | None = None
34
33
 
35
34
  # Event-specific data
36
- prompt: Optional[str] = None # PROMPT events
37
- file_path: Optional[str] = None # FILE_READ events
38
- mcp_server_name: Optional[str] = None # MCP_EXECUTION events
39
- mcp_tool_name: Optional[str] = None
40
- mcp_arguments: Optional[dict] = None
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, Optional
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) -> Optional[dict]:
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: Optional[str] = None) -> dict:
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, Optional, Union
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: Optional[Union[str, AiHookEventType]],
49
+ event_name: str | AiHookEventType | None,
50
50
  user_message: str,
51
- agent_message: Optional[str] = None,
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: Optional[dict]) -> bool:
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, Optional
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: Optional[GuardrailConfig]) -> float:
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: Optional[str], ttl_seconds: float) -> bool:
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: Optional[str]) -> None:
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: Optional[str],
94
- tenant_id: Optional[str],
95
- config: Optional[GuardrailConfig],
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: Optional[str]) -> Optional[GuardrailConfig]:
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, Optional
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
- Optional[Path],
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, Optional
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
- Optional[Path],
31
+ Path | None,
32
32
  typer.Option(
33
33
  '--repo-path',
34
34
  help='Repository path for repo-scoped uninstallation (defaults to current directory).',
@@ -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, Optional
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: Optional[click.Context]) -> None:
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) -> Optional[click.Command]:
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, Optional
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[len(common_prefix) :] if path.startswith(common_prefix) else 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: Optional[str] = None,
95
- client_secret: Optional[str] = None,
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: Optional[str] = None,
119
- client_secret: Optional[str] = None,
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: Optional[str] = None, client_secret: Optional[str] = None) -> dict:
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() -> Optional[dict]:
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: Optional[str] = None, client_secret: Optional[str] = None) -> tuple[str, str]:
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: Optional[str] = None, client_secret: Optional[str] = None) -> dict:
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, Optional
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') -> Optional[AuthInfo]:
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: Optional[str], id_token: Optional[str]
54
- ) -> Optional[AuthInfo]:
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: Optional[str],
22
- new_value: Optional[str],
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() -> Optional[str]:
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() -> Optional[str]:
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