cycode 3.22.2.dev3__py3-none-any.whl → 3.23.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cycode/__init__.py CHANGED
@@ -5,4 +5,4 @@ import time as _time
5
5
  # end-to-end scan duration from the moment the user actually triggered it.
6
6
  _BOOT_WALL: float = _time.time()
7
7
 
8
- __version__ = '3.22.2.dev3' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
8
+ __version__ = '3.23.1.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -22,6 +22,18 @@ class GuardrailsMode(str, Enum):
22
22
  BLOCK = 'block'
23
23
 
24
24
 
25
+ class GuardrailCellMode(str, Enum):
26
+ """A guardrail x agent cell in the platform-resolved matrix.
27
+
28
+ Separate from GuardrailsMode because Off is a platform-only state: it is not an
29
+ install `--mode` choice, and an off guardrail reports no mode to the server.
30
+ """
31
+
32
+ OFF = 'off'
33
+ REPORT = GuardrailsMode.REPORT.value
34
+ BLOCK = GuardrailsMode.BLOCK.value
35
+
36
+
25
37
  # Base CLI commands invoked from installed hooks. IDE classes append --ide flags
26
38
  # (and any other suffix) on top of these.
27
39
  CYCODE_SCAN_PROMPT_COMMAND = 'cycode ai-guardrails scan'
@@ -12,9 +12,9 @@ from typing import Optional
12
12
 
13
13
  import yaml
14
14
 
15
- from cycode.cli.apps.ai_guardrails.consts import PolicyMode
16
15
  from cycode.cli.apps.ai_guardrails.ides.base import IDE
17
16
  from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_POLICY, POLICY_FILE_NAME
17
+ from cycode.cli.apps.ai_guardrails.scan.policy import strip_platform_managed_keys
18
18
  from cycode.logger import get_logger
19
19
 
20
20
  logger = get_logger('AI Guardrails Hooks')
@@ -102,22 +102,21 @@ def _load_policy_dict(policy_path: Path) -> dict:
102
102
  return {**copy.deepcopy(DEFAULT_POLICY), **existing}
103
103
 
104
104
 
105
- def create_policy_file(scope: str, mode: PolicyMode, repo_path: Optional[Path] = None) -> tuple[bool, str]:
106
- """Create or update the ai-guardrails.yaml policy file.
105
+ def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
106
+ """Create or update the ai-guardrails.yaml policy file (operational knobs only).
107
107
 
108
- If the file already exists, only the mode field is updated; otherwise a new
109
- file is created from the default policy.
108
+ Enforcement mode and sensitive-path globs are platform-managed; those keys are stripped
109
+ (including ones an older CLI wrote), everything else the user customized is preserved.
110
110
  """
111
111
  config_dir = repo_path / '.cycode' if scope == 'repo' and repo_path else Path.home() / '.cycode'
112
112
  policy_path = config_dir / POLICY_FILE_NAME
113
113
 
114
- policy = _load_policy_dict(policy_path)
115
- policy['mode'] = mode.value
114
+ policy = strip_platform_managed_keys(_load_policy_dict(policy_path))
116
115
 
117
116
  try:
118
117
  config_dir.mkdir(parents=True, exist_ok=True)
119
118
  policy_path.write_text(yaml.dump(policy, default_flow_style=False, sort_keys=False), encoding='utf-8')
120
- return True, f'AI guardrails policy ({mode.value} mode) set: {policy_path}'
119
+ return True, f'AI guardrails policy file set: {policy_path}'
121
120
  except Exception as e:
122
121
  logger.error('Failed to create policy file', exc_info=e)
123
122
  return False, f'Failed to create policy file: {policy_path}'
@@ -6,7 +6,7 @@ from typing import Annotated, Optional
6
6
  import typer
7
7
 
8
8
  from cycode.cli.apps.ai_guardrails.command_utils import console, resolve_repo_path, validate_scope
9
- from cycode.cli.apps.ai_guardrails.consts import GuardrailsMode, PolicyMode
9
+ from cycode.cli.apps.ai_guardrails.consts import GuardrailsMode
10
10
  from cycode.cli.apps.ai_guardrails.hooks_manager import create_policy_file, install_hooks
11
11
  from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, IDES, resolve_ides
12
12
 
@@ -44,8 +44,7 @@ def install_command(
44
44
  typer.Option(
45
45
  '--mode',
46
46
  '-m',
47
- help='Installation mode: "report" for async non-blocking hooks with warn policy, '
48
- '"block" for sync blocking hooks.',
47
+ help='[Deprecated] Enforcement mode is platform-managed; configure guardrails in the Cycode platform.',
49
48
  ),
50
49
  ] = GuardrailsMode.REPORT,
51
50
  ) -> None:
@@ -80,32 +79,34 @@ def install_command(
80
79
  console.print(f'[red]✗[/] {message}', style='bold red')
81
80
  all_success = False
82
81
 
82
+ if mode == GuardrailsMode.BLOCK:
83
+ console.print(
84
+ '[yellow]--mode is deprecated:[/] enforcement mode is platform-managed; '
85
+ 'configure guardrails in the Cycode platform.'
86
+ )
87
+
83
88
  if any_success:
84
- policy_mode = PolicyMode.WARN if mode == GuardrailsMode.REPORT else PolicyMode.BLOCK
85
- _install_policy(scope, repo_path, policy_mode)
86
- _print_next_steps(results, mode)
89
+ _install_policy(scope, repo_path)
90
+ _print_next_steps(results)
87
91
 
88
92
  if not all_success:
89
93
  raise typer.Exit(1)
90
94
 
91
95
 
92
- def _install_policy(scope: str, repo_path: Optional[Path], policy_mode: PolicyMode) -> None:
93
- policy_success, policy_message = create_policy_file(scope, policy_mode, repo_path)
96
+ def _install_policy(scope: str, repo_path: Optional[Path]) -> None:
97
+ policy_success, policy_message = create_policy_file(scope, repo_path)
94
98
  if policy_success:
95
99
  console.print(f'[green]✓[/] {policy_message}')
96
100
  else:
97
101
  console.print(f'[red]✗[/] {policy_message}', style='bold red')
98
102
 
99
103
 
100
- def _print_next_steps(results: list[tuple[str, bool, str]], mode: GuardrailsMode) -> None:
104
+ def _print_next_steps(results: list[tuple[str, bool, str]]) -> None:
101
105
  console.print()
102
106
  console.print('[bold]Next steps:[/]')
103
107
  successful_ides = [name for name, success, _ in results if success]
104
108
  ide_list = ', '.join(successful_ides)
105
109
  console.print(f'1. Restart {ide_list} to activate the hooks')
106
- console.print('2. (Optional) Customize policy in ~/.cycode/ai-guardrails.yaml')
110
+ console.print('2. Configure guardrail enforcement in the Cycode platform')
107
111
  console.print()
108
- if mode == GuardrailsMode.REPORT:
109
- console.print('[dim]Report mode: policy is set to warn.[/]')
110
- else:
111
- console.print('[dim]The hooks will scan prompts, file reads, and MCP tool calls for secrets.[/]')
112
+ console.print('[dim]The hooks will scan prompts, file reads, and MCP tool calls for secrets.[/]')
@@ -1,48 +1,37 @@
1
1
  """
2
2
  Constants and default configuration for AI guardrails.
3
3
 
4
- These defaults can be overridden by:
5
- 1. User-level config: ~/.cycode/ai-guardrails.yaml
6
- 2. Repo-level config: <workspace>/.cycode/ai-guardrails.yaml
4
+ Enforcement (which guardrails run, in which mode, over which paths) is platform-owned and
5
+ resolved per scan; see scan/guardrail_config.py. What is left here is the operational knobs
6
+ a local file may override - user-level ~/.cycode/ai-guardrails.yaml, then repo-level
7
+ <workspace>/.cycode/ai-guardrails.yaml.
7
8
  """
8
9
 
9
10
  # Policy file name
10
11
  POLICY_FILE_NAME = 'ai-guardrails.yaml'
11
12
 
12
- # Default policy configuration
13
+ # Sensitive-path globs used until the platform's own list is cached (cold start, or a tenant
14
+ # that never customized them). Not a local knob: apply_platform_config always overwrites it.
15
+ DEFAULT_SENSITIVE_PATH_GLOBS = [
16
+ '.env',
17
+ '.env.*',
18
+ '*.pem',
19
+ '*.p12',
20
+ '*.key',
21
+ '.aws/**',
22
+ '.ssh/**',
23
+ '*kubeconfig*',
24
+ '.npmrc',
25
+ '.netrc',
26
+ ]
27
+
28
+ # Default policy configuration: operational knobs only.
13
29
  DEFAULT_POLICY = {
14
30
  'version': 1,
15
- 'mode': 'block', # block | warn
16
31
  'fail_open': True, # allow if scan fails/timeouts
17
32
  'secrets': {
18
33
  'scan_type': 'secret',
19
34
  'timeout_ms': 30000,
20
35
  'max_bytes': 200000,
21
36
  },
22
- 'prompt': {
23
- 'enabled': True,
24
- 'action': 'block',
25
- },
26
- 'file_read': {
27
- 'enabled': True,
28
- 'action': 'block',
29
- 'deny_globs': [
30
- '.env',
31
- '.env.*',
32
- '*.pem',
33
- '*.p12',
34
- '*.key',
35
- '.aws/**',
36
- '.ssh/**',
37
- '*kubeconfig*',
38
- '.npmrc',
39
- '.netrc',
40
- ],
41
- 'scan_content': True,
42
- },
43
- 'mcp': {
44
- 'enabled': True,
45
- 'action': 'block',
46
- 'scan_arguments': True,
47
- },
48
37
  }
@@ -0,0 +1,165 @@
1
+ """Platform guardrail config cache.
2
+
3
+ session-start fetches the tenant's resolved guardrail config from the platform and writes it
4
+ here; scans only read. Per-agent modes and sensitive-path globs are platform-owned - local
5
+ policy files never carry them. An absent or corrupt cache means built-in defaults (Report
6
+ everywhere + the default globs), always synchronous.
7
+ """
8
+
9
+ import json
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from cycode.cli.apps.ai_guardrails.consts import GuardrailCellMode, PolicyMode
16
+ from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_SENSITIVE_PATH_GLOBS
17
+ from cycode.cli.apps.ai_guardrails.scan.types import BlockReason
18
+ from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY
19
+ from cycode.cli.utils.path_utils import atomic_write_text, quarantine_corrupt_file
20
+ from cycode.logger import get_logger
21
+
22
+ logger = get_logger('AI Guardrails')
23
+
24
+ GUARDRAILS_CONFIG_FILE_NAME = 'ai-guardrails-config.json'
25
+
26
+ _DEFAULT_TTL_SECONDS = 900
27
+
28
+ # Guardrail keys are the CLI's block-reason vocabulary. Anything else in the payload (a future
29
+ # guardrail this CLI doesn't implement) is ignored - unknown config must never fail closed.
30
+ _KNOWN_GUARDRAIL_KEYS = frozenset(
31
+ reason.value
32
+ for reason in (
33
+ BlockReason.SECRETS_IN_PROMPT,
34
+ BlockReason.SECRETS_IN_FILE,
35
+ BlockReason.SENSITIVE_PATH,
36
+ BlockReason.SECRETS_IN_MCP_ARGS,
37
+ )
38
+ )
39
+
40
+ # CLI --ide names to matrix column names; identity for names not listed.
41
+ _AGENT_BY_IDE_NAME = {'claude-code': 'claude'}
42
+
43
+
44
+ def get_config_cache_path() -> Path:
45
+ return Path.home() / CYCODE_CONFIGURATION_DIRECTORY / GUARDRAILS_CONFIG_FILE_NAME
46
+
47
+
48
+ def agent_for_ide(ide_name: Optional[str]) -> str:
49
+ ide_name = (ide_name or '').lower()
50
+ return _AGENT_BY_IDE_NAME.get(ide_name, ide_name)
51
+
52
+
53
+ def _default_sensitive_globs() -> list:
54
+ return list(DEFAULT_SENSITIVE_PATH_GLOBS)
55
+
56
+
57
+ @dataclass
58
+ class GuardrailConfig:
59
+ payload: dict
60
+ fetched_at: float
61
+ tenant_id: Optional[str] = None
62
+ _guardrails: dict = field(init=False, repr=False)
63
+
64
+ def __post_init__(self) -> None:
65
+ self._guardrails = {
66
+ guardrail.get('key'): guardrail
67
+ for guardrail in self.payload.get('guardrails') or []
68
+ if guardrail.get('key') in _KNOWN_GUARDRAIL_KEYS
69
+ }
70
+
71
+ def mode_for(self, guardrail_key: str, ide_name: Optional[str]) -> str:
72
+ agents = (self._guardrails.get(guardrail_key) or {}).get('agents') or {}
73
+ return str(agents.get(agent_for_ide(ide_name), GuardrailCellMode.REPORT.value)).lower()
74
+
75
+ def _modes_for_event(self, event_name: str, ide_name: Optional[str]) -> list:
76
+ return [
77
+ self.mode_for(key, ide_name)
78
+ for key, guardrail in self._guardrails.items()
79
+ if str(guardrail.get('event_type', '')).lower() == str(event_name).lower()
80
+ ]
81
+
82
+ def is_event_off(self, event_name: str, ide_name: Optional[str]) -> bool:
83
+ """Every guardrail for this event is Off - skip the scan entirely."""
84
+ modes = self._modes_for_event(event_name, ide_name)
85
+ return bool(modes) and all(mode == GuardrailCellMode.OFF for mode in modes)
86
+
87
+ def can_event_block(self, event_name: str, ide_name: Optional[str]) -> bool:
88
+ """At least one guardrail for this event is in Block mode - the scan must stay synchronous."""
89
+ return GuardrailCellMode.BLOCK in self._modes_for_event(event_name, ide_name)
90
+
91
+ def sensitive_globs(self) -> list:
92
+ settings = (self._guardrails.get(BlockReason.SENSITIVE_PATH) or {}).get('settings') or {}
93
+ globs = settings.get('globs')
94
+ return globs if isinstance(globs, list) and globs else _default_sensitive_globs()
95
+
96
+ def is_expired(self) -> bool:
97
+ ttl = self.payload.get('ttl_seconds') or _DEFAULT_TTL_SECONDS
98
+ return time.time() - self.fetched_at > ttl
99
+
100
+ def needs_refresh(self, tenant_id: Optional[str]) -> bool:
101
+ """Expired, or fetched for another tenant (the user switched tenants since)."""
102
+ return self.is_expired() or self.tenant_id != tenant_id
103
+
104
+
105
+ def apply_platform_config(policy: dict, config: Optional[GuardrailConfig], ide_name: Optional[str]) -> None:
106
+ """Overlay the platform-owned enforcement config onto the local knobs-only policy.
107
+
108
+ The platform is the only mode source: no cache (cold start) means the built-in defaults -
109
+ Report everywhere with the default globs - which equal an unconfigured tenant's platform
110
+ config, so behaviour is uniform either way. Each matrix cell lands on its own per-feature
111
+ action, so the two FileRead guardrails (content scan vs. sensitive path) keep independent modes.
112
+ An all-Off event never reaches here at all: scan_command skips it.
113
+ """
114
+
115
+ def cell(guardrail_key: str) -> str:
116
+ return config.mode_for(guardrail_key, ide_name) if config is not None else GuardrailCellMode.REPORT.value
117
+
118
+ def action(guardrail_key: str) -> str:
119
+ return PolicyMode.BLOCK.value if cell(guardrail_key) == GuardrailCellMode.BLOCK else PolicyMode.WARN.value
120
+
121
+ policy.setdefault('prompt', {})['action'] = action(BlockReason.SECRETS_IN_PROMPT)
122
+
123
+ file_read = policy.setdefault('file_read', {})
124
+ file_read['scan_content'] = cell(BlockReason.SECRETS_IN_FILE) != GuardrailCellMode.OFF
125
+ file_read['action'] = action(BlockReason.SECRETS_IN_FILE)
126
+ file_read['deny_globs'] = (
127
+ (config.sensitive_globs() if config is not None else _default_sensitive_globs())
128
+ if cell(BlockReason.SENSITIVE_PATH) != GuardrailCellMode.OFF
129
+ else []
130
+ )
131
+ file_read['path_action'] = action(BlockReason.SENSITIVE_PATH)
132
+
133
+ policy.setdefault('mcp', {})['action'] = action(BlockReason.SECRETS_IN_MCP_ARGS)
134
+
135
+
136
+ def save_guardrail_config(payload: dict, tenant_id: Optional[str]) -> None:
137
+ """Persist a fetched resolved config; a failed write just leaves the previous cache in place."""
138
+ path = get_config_cache_path()
139
+ content = {'fetched_at': time.time(), 'tenant_id': tenant_id, 'payload': payload}
140
+ try:
141
+ path.parent.mkdir(parents=True, exist_ok=True)
142
+ atomic_write_text(str(path), json.dumps(content))
143
+ except Exception as e:
144
+ logger.debug('Failed to save guardrail config cache', exc_info=e)
145
+
146
+
147
+ def load_guardrail_config() -> Optional[GuardrailConfig]:
148
+ """The cached platform config, or None when it is absent or corrupt (quarantined)."""
149
+ path = get_config_cache_path()
150
+ if not path.exists():
151
+ return None
152
+
153
+ try:
154
+ with open(path, encoding='UTF-8') as file:
155
+ content = json.load(file)
156
+ payload = content['payload']
157
+ if not isinstance(payload, dict):
158
+ raise ValueError('payload is not an object')
159
+ return GuardrailConfig(
160
+ payload=payload, fetched_at=float(content['fetched_at']), tenant_id=content.get('tenant_id')
161
+ )
162
+ except Exception as e:
163
+ logger.warning('Guardrail config cache is corrupt and will be moved aside', exc_info=e)
164
+ quarantine_corrupt_file(str(path))
165
+ return None
@@ -13,10 +13,13 @@ import os
13
13
  from dataclasses import dataclass
14
14
  from multiprocessing.pool import ThreadPool
15
15
  from multiprocessing.pool import TimeoutError as PoolTimeoutError
16
- from typing import Callable, Optional
16
+ from typing import TYPE_CHECKING, Callable, Optional
17
17
 
18
18
  import typer
19
19
 
20
+ if TYPE_CHECKING:
21
+ from cycode.cli.apps.ai_guardrails.scan.guardrail_config import GuardrailConfig
22
+
20
23
  from cycode.cli.apps.ai_guardrails.consts import GuardrailsMode, PolicyMode
21
24
  from cycode.cli.apps.ai_guardrails.ides.base import HookDecision
22
25
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
@@ -48,11 +51,7 @@ def handle_before_submit_prompt(ctx: typer.Context, payload: AIHookPayload, poli
48
51
  ai_client = ctx.obj['ai_security_client']
49
52
 
50
53
  prompt_config = get_policy_value(policy, 'prompt', default={})
51
- if not get_policy_value(prompt_config, 'enabled', default=True):
52
- ai_client.create_event(payload, AiHookEventType.PROMPT, AIHookOutcome.ALLOWED)
53
- return HookDecision.allow(AiHookEventType.PROMPT)
54
-
55
- effective_mode = get_effective_mode(policy, prompt_config)
54
+ effective_mode = get_effective_mode(prompt_config)
56
55
  prompt = payload.prompt or ''
57
56
  max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000)
58
57
  timeout_ms = get_policy_value(policy, 'secrets', 'timeout_ms', default=30000)
@@ -104,12 +103,9 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
104
103
  ai_client = ctx.obj['ai_security_client']
105
104
 
106
105
  file_read_config = get_policy_value(policy, 'file_read', default={})
107
- if not get_policy_value(file_read_config, 'enabled', default=True):
108
- ai_client.create_event(payload, AiHookEventType.FILE_READ, AIHookOutcome.ALLOWED)
109
- return HookDecision.allow(AiHookEventType.FILE_READ)
110
-
111
106
  file_path = payload.file_path or ''
112
- effective_mode = get_effective_mode(policy, file_read_config)
107
+ path_mode = get_effective_mode(file_read_config, action_key='path_action')
108
+ content_mode = get_effective_mode(file_read_config)
113
109
 
114
110
  scan_id = None
115
111
  block_reason = None
@@ -120,7 +116,7 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
120
116
  is_sensitive_path = is_denied_path(file_path, policy)
121
117
  if is_sensitive_path:
122
118
  block_reason = BlockReason.SENSITIVE_PATH
123
- if effective_mode == GuardrailsMode.BLOCK:
119
+ if path_mode == GuardrailsMode.BLOCK:
124
120
  outcome = AIHookOutcome.BLOCKED
125
121
  user_message = f'Cycode blocked sending {file_path} to the AI (sensitive path policy).'
126
122
  return HookDecision.deny(
@@ -144,11 +140,11 @@ def handle_before_read_file(ctx: typer.Context, payload: AIHookPayload, policy:
144
140
 
145
141
  if get_policy_value(file_read_config, 'scan_content', default=True):
146
142
  violation_summary, scan_id = _scan_path_for_secrets(
147
- ctx, file_path, policy, payload=payload, effective_mode=effective_mode
143
+ ctx, file_path, policy, payload=payload, effective_mode=content_mode
148
144
  )
149
145
  if violation_summary:
150
146
  block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[AiHookEventType.FILE_READ]
151
- if effective_mode == GuardrailsMode.BLOCK:
147
+ if content_mode == GuardrailsMode.BLOCK:
152
148
  outcome = AIHookOutcome.BLOCKED
153
149
  user_message = f'Cycode blocked reading {file_path}. {violation_summary}'
154
150
  return HookDecision.deny(
@@ -201,7 +197,6 @@ class _ArgScanFeature:
201
197
  """
202
198
 
203
199
  policy_key: str # 'mcp' or 'command_exec'
204
- scan_key: str # 'scan_arguments' or 'scan_command'
205
200
  event_type: AiHookEventType
206
201
  deny_message: Callable[[str], str]
207
202
  deny_agent_message: str
@@ -220,14 +215,10 @@ def _handle_arg_scan(
220
215
  ai_client = ctx.obj['ai_security_client']
221
216
 
222
217
  feature_config = get_policy_value(policy, feature.policy_key, default={})
223
- if not get_policy_value(feature_config, 'enabled', default=True):
224
- ai_client.create_event(payload, feature.event_type, AIHookOutcome.ALLOWED)
225
- return HookDecision.allow(feature.event_type)
226
-
227
218
  max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000)
228
219
  timeout_ms = get_policy_value(policy, 'secrets', 'timeout_ms', default=30000)
229
220
  clipped = truncate_utf8(scan_text, max_bytes)
230
- effective_mode = get_effective_mode(policy, feature_config)
221
+ effective_mode = get_effective_mode(feature_config)
231
222
 
232
223
  scan_id = None
233
224
  block_reason = None
@@ -235,30 +226,29 @@ def _handle_arg_scan(
235
226
  error_message = None
236
227
 
237
228
  try:
238
- if get_policy_value(feature_config, feature.scan_key, default=True):
239
- violation_summary, scan_id = _scan_text_for_secrets(
240
- ctx,
241
- clipped,
242
- timeout_ms,
243
- payload=payload,
244
- event_type=feature.event_type,
245
- effective_mode=effective_mode,
246
- )
247
- if violation_summary:
248
- block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[feature.event_type]
249
- if effective_mode == GuardrailsMode.BLOCK:
250
- outcome = AIHookOutcome.BLOCKED
251
- return HookDecision.deny(
252
- feature.event_type,
253
- feature.deny_message(violation_summary),
254
- feature.deny_agent_message,
255
- )
256
- outcome = AIHookOutcome.WARNED
257
- return HookDecision.ask(
229
+ violation_summary, scan_id = _scan_text_for_secrets(
230
+ ctx,
231
+ clipped,
232
+ timeout_ms,
233
+ payload=payload,
234
+ event_type=feature.event_type,
235
+ effective_mode=effective_mode,
236
+ )
237
+ if violation_summary:
238
+ block_reason = SECRETS_BLOCK_REASON_BY_EVENT_TYPE[feature.event_type]
239
+ if effective_mode == GuardrailsMode.BLOCK:
240
+ outcome = AIHookOutcome.BLOCKED
241
+ return HookDecision.deny(
258
242
  feature.event_type,
259
- feature.ask_message(violation_summary),
260
- feature.ask_agent_message,
243
+ feature.deny_message(violation_summary),
244
+ feature.deny_agent_message,
261
245
  )
246
+ outcome = AIHookOutcome.WARNED
247
+ return HookDecision.ask(
248
+ feature.event_type,
249
+ feature.ask_message(violation_summary),
250
+ feature.ask_agent_message,
251
+ )
262
252
 
263
253
  return HookDecision.allow(feature.event_type)
264
254
  except Exception as e:
@@ -290,7 +280,6 @@ def handle_before_mcp_execution(ctx: typer.Context, payload: AIHookPayload, poli
290
280
  policy,
291
281
  _ArgScanFeature(
292
282
  policy_key='mcp',
293
- scan_key='scan_arguments',
294
283
  event_type=AiHookEventType.MCP_EXECUTION,
295
284
  deny_message=lambda v: f'Cycode blocked MCP tool call "{tool}". {v}',
296
285
  deny_agent_message='Do not pass secrets to tools. Use secret references (name/id) instead.',
@@ -311,35 +300,29 @@ def get_handler_for_event(event_type: str) -> Optional[HandlerFn]:
311
300
  return handlers.get(event_type)
312
301
 
313
302
 
314
- def get_effective_mode(policy: dict, feature_config: dict) -> GuardrailsMode:
315
- """The event only blocks when both the global mode and the per-guardrail action are block."""
316
- mode = get_policy_value(policy, 'mode', default=PolicyMode.BLOCK)
317
- action = get_policy_value(feature_config, 'action', default=PolicyMode.BLOCK)
318
- return GuardrailsMode.BLOCK if (mode == PolicyMode.BLOCK and action == PolicyMode.BLOCK) else GuardrailsMode.REPORT
319
-
303
+ def get_effective_mode(feature_config: dict, action_key: str = 'action') -> GuardrailsMode:
304
+ """A guardrail's action is its matrix cell: block, or warn (report) for everything else."""
305
+ action = get_policy_value(feature_config, action_key, default=PolicyMode.BLOCK)
306
+ return GuardrailsMode.BLOCK if action == PolicyMode.BLOCK else GuardrailsMode.REPORT
320
307
 
321
- # The policy section each event's handler reads its feature config from.
322
- _FEATURE_KEY_BY_EVENT_TYPE: dict[str, str] = {
323
- AiHookEventType.PROMPT.value: 'prompt',
324
- AiHookEventType.FILE_READ.value: 'file_read',
325
- AiHookEventType.MCP_EXECUTION.value: 'mcp',
326
- }
327
308
 
328
-
329
- def should_detach_scan(policy: dict, event_name: str) -> bool:
309
+ def should_detach_scan(
310
+ config: Optional['GuardrailConfig'],
311
+ policy: dict,
312
+ event_name: str,
313
+ ide_name: Optional[str],
314
+ ) -> bool:
330
315
  """Whether this event's scan is safe to run detached.
331
316
 
332
- Report mode never blocks, so nobody consumes the verdict. Fail-closed
333
- configs stay synchronous even in report mode: their deny on scan failure
334
- must reach the IDE. Unknown events stay synchronous - they exit fast anyway.
317
+ Report mode never blocks, so nobody consumes the verdict. The platform config is the only
318
+ mode source: without a cache the scan stays synchronous (never detach on an assumption).
319
+ Fail-closed configs also stay synchronous: their deny on scan failure must reach the IDE.
335
320
  """
336
- feature_key = _FEATURE_KEY_BY_EVENT_TYPE.get(event_name)
337
- if feature_key is None:
321
+ if config is None:
338
322
  return False
339
323
  if not get_policy_value(policy, 'fail_open', default=True):
340
324
  return False
341
- feature_config = get_policy_value(policy, feature_key, default={})
342
- return get_effective_mode(policy, feature_config) == GuardrailsMode.REPORT
325
+ return not config.can_event_block(event_name, ide_name)
343
326
 
344
327
 
345
328
  def build_ai_guardrails_scan_parameters(
@@ -1,11 +1,14 @@
1
1
  """
2
2
  Policy loading and configuration management for AI guardrails.
3
3
 
4
- Policies are loaded and merged in order (later overrides earlier):
4
+ Operational knobs are loaded and merged in order (later overrides earlier):
5
5
  1. Built-in defaults (consts.DEFAULT_POLICY)
6
6
  2. Machine-wide config (admin/MDM-provisioned; see get_machine_policy_path)
7
7
  3. User-level config (~/.cycode/ai-guardrails.yaml)
8
8
  4. Repo-level config (<workspace>/.cycode/ai-guardrails.yaml)
9
+
10
+ Enforcement is not part of that merge: the platform resolves it and apply_platform_config
11
+ overlays it on top of the result (see scan/guardrail_config.py).
9
12
  """
10
13
 
11
14
  import json
@@ -17,6 +20,34 @@ from typing import Any, Optional
17
20
  import yaml
18
21
 
19
22
  from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_POLICY, POLICY_FILE_NAME
23
+ from cycode.logger import get_logger
24
+
25
+ logger = get_logger('AI Guardrails')
26
+
27
+ # A local policy file may only contribute operational knobs; enforcement comes from the
28
+ # platform. Read as a whitelist rather than a strip-list, so a key we forget to enumerate
29
+ # is ignored by default instead of silently weakening enforcement.
30
+ _LOCAL_POLICY_KEYS = ('version', 'fail_open', 'secrets')
31
+
32
+ # The sections the platform resolves. Only used when rewriting a user's file: keys an older
33
+ # CLI wrote there still read as enforcement to a human, so drop them on the way out.
34
+ _PLATFORM_OWNED_KEYS = ('mode', 'prompt', 'file_read', 'mcp')
35
+
36
+
37
+ def pick_local_knobs(config: dict, filename: str) -> dict:
38
+ """Keep only the keys a local policy file is allowed to contribute."""
39
+ ignored = [key for key in config if key not in _LOCAL_POLICY_KEYS]
40
+ if ignored:
41
+ logger.debug(
42
+ 'Ignoring non-knob keys in local policy file, %s',
43
+ {'filename': filename, 'keys': ignored},
44
+ )
45
+ return {key: value for key, value in config.items() if key in _LOCAL_POLICY_KEYS}
46
+
47
+
48
+ def strip_platform_managed_keys(config: dict) -> dict:
49
+ """Drop the enforcement sections a local file may carry (older CLIs wrote them)."""
50
+ return {key: value for key, value in config.items() if key not in _PLATFORM_OWNED_KEYS}
20
51
 
21
52
 
22
53
  def get_machine_policy_path() -> Path:
@@ -83,21 +114,22 @@ def load_policy(workspace_root: Optional[str] = None) -> dict:
83
114
  policy = load_defaults()
84
115
 
85
116
  # Merge machine-wide config (admin/MDM-provisioned) - overrides defaults, below user/repo.
86
- machine_config = load_yaml_file(get_machine_policy_path())
117
+ machine_policy_path = get_machine_policy_path()
118
+ machine_config = load_yaml_file(machine_policy_path)
87
119
  if machine_config:
88
- policy = deep_merge(policy, machine_config)
120
+ policy = deep_merge(policy, pick_local_knobs(machine_config, str(machine_policy_path)))
89
121
 
90
122
  # Merge user-level config (if exists)
91
123
  user_policy_path = Path.home() / '.cycode' / POLICY_FILE_NAME
92
124
  user_config = load_yaml_file(user_policy_path)
93
125
  if user_config:
94
- policy = deep_merge(policy, user_config)
126
+ policy = deep_merge(policy, pick_local_knobs(user_config, str(user_policy_path)))
95
127
 
96
128
  # Merge repo-level config (if exists) - highest precedence
97
129
  if workspace_root:
98
130
  repo_policy_path = Path(workspace_root) / '.cycode' / POLICY_FILE_NAME
99
131
  repo_config = load_yaml_file(repo_policy_path)
100
132
  if repo_config:
101
- policy = deep_merge(policy, repo_config)
133
+ policy = deep_merge(policy, pick_local_knobs(repo_config, str(repo_policy_path)))
102
134
 
103
135
  return policy
@@ -14,8 +14,9 @@ import click
14
14
  import typer
15
15
 
16
16
  from cycode.cli.apps.ai_guardrails.ides import DEFAULT_IDE_NAME, get_ide
17
- from cycode.cli.apps.ai_guardrails.ides.base import HookDecision
17
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE, HookDecision
18
18
  from cycode.cli.apps.ai_guardrails.scan.detach import is_detached_child, respawn_detached
19
+ from cycode.cli.apps.ai_guardrails.scan.guardrail_config import apply_platform_config, load_guardrail_config
19
20
  from cycode.cli.apps.ai_guardrails.scan.handlers import get_handler_for_event, should_detach_scan
20
21
  from cycode.cli.apps.ai_guardrails.scan.policy import load_policy
21
22
  from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
@@ -61,6 +62,31 @@ def _deny_for_event(
61
62
  return HookDecision.deny(target, user_message, agent_message)
62
63
 
63
64
 
65
+ def _should_skip_payload(ide_integration: IDE, payload: Optional[dict]) -> bool:
66
+ """Fast exits that never scan: empty/foreign/synthetic payloads all answer a plain allow."""
67
+ if not payload:
68
+ logger.debug('Empty or invalid JSON payload received')
69
+ return True
70
+
71
+ # Prevent cross-IDE processing (e.g. Cursor reading Claude Code hooks
72
+ # from ~/.claude/settings.json).
73
+ if not ide_integration.matches_payload(payload):
74
+ logger.debug(
75
+ 'Payload event does not match expected IDE, skipping',
76
+ extra={'hook_event_name': payload.get('hook_event_name'), 'expected_ide': ide_integration.name},
77
+ )
78
+ return True
79
+
80
+ # Fork/subagent completions arrive as synthetic user turns (e.g. Claude Code's
81
+ # <task-notification>); they are agent-generated, not user prompts - skip before
82
+ # parse_hook_payload, which reads the transcript and IDE config from disk.
83
+ if ide_integration.is_synthetic_prompt(payload):
84
+ logger.debug('Synthetic prompt detected, skipping scan')
85
+ return True
86
+
87
+ return False
88
+
89
+
64
90
  def _initialize_clients(ctx: typer.Context) -> None:
65
91
  """Initialize API clients.
66
92
 
@@ -95,26 +121,7 @@ def scan_command(
95
121
  stdin_data = read_stdin_text().strip()
96
122
  payload = safe_json_parse(stdin_data)
97
123
 
98
- if not payload:
99
- logger.debug('Empty or invalid JSON payload received')
100
- output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
101
- return
102
-
103
- # Prevent cross-IDE processing (e.g. Cursor reading Claude Code hooks
104
- # from ~/.claude/settings.json).
105
- if not ide_integration.matches_payload(payload):
106
- logger.debug(
107
- 'Payload event does not match expected IDE, skipping',
108
- extra={'hook_event_name': payload.get('hook_event_name'), 'expected_ide': ide_integration.name},
109
- )
110
- output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
111
- return
112
-
113
- # Fork/subagent completions arrive as synthetic user turns (e.g. Claude Code's
114
- # <task-notification>); they are agent-generated, not user prompts - skip before
115
- # parse_hook_payload, which reads the transcript and IDE config from disk.
116
- if ide_integration.is_synthetic_prompt(payload):
117
- logger.debug('Synthetic prompt detected, skipping scan')
124
+ if _should_skip_payload(ide_integration, payload):
118
125
  output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
119
126
  return
120
127
 
@@ -136,14 +143,27 @@ def scan_command(
136
143
  output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
137
144
  return
138
145
 
146
+ # Scans only read the cache; session-start refreshes it, so the hot path never waits on the
147
+ # network. Every guardrail for this event Off: skip entirely - no scan, no event, no auth.
148
+ config = load_guardrail_config()
149
+ if config is not None and config.is_event_off(event_name, ide_integration.name):
150
+ logger.debug('Guardrails are off for this event, allowing', extra={'event_name': event_name})
151
+ output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
152
+ return
153
+
139
154
  # `or` (not a .get default) - Cursor sends workspace_roots=[] when no folder is open.
140
155
  workspace_roots = payload.get('workspace_roots') or ['.']
141
156
  policy = load_policy(workspace_roots[0])
157
+ apply_platform_config(policy, config, ide_integration.name)
142
158
 
143
159
  # Report mode: nobody consumes the verdict, so hand the scan to a detached
144
160
  # child and release the IDE immediately. Runs before any client or network
145
161
  # work. A failed respawn falls through to the synchronous path.
146
- if not is_detached_child() and should_detach_scan(policy, event_name) and respawn_detached(stdin_data):
162
+ if (
163
+ not is_detached_child()
164
+ and should_detach_scan(config, policy, event_name, ide_integration.name)
165
+ and respawn_detached(stdin_data)
166
+ ):
147
167
  return
148
168
 
149
169
  try:
@@ -15,6 +15,7 @@ from cycode.cli.apps.ai_guardrails.ides import (
15
15
  collect_all_skills,
16
16
  get_ide,
17
17
  )
18
+ from cycode.cli.apps.ai_guardrails.scan.guardrail_config import load_guardrail_config, save_guardrail_config
18
19
  from cycode.cli.apps.ai_guardrails.scan.utils import read_stdin_text, safe_json_parse
19
20
  from cycode.cli.apps.auth.auth_common import get_authorization_info
20
21
  from cycode.cli.apps.auth.auth_manager import AuthManager
@@ -167,3 +168,23 @@ def session_start_command(
167
168
 
168
169
  # Report session context (device + cross-IDE MCP servers and plugins)
169
170
  _report_session_context(ai_client, session_payload.ide_user_email, auth_info.tenant_id)
171
+
172
+ # SessionStart precedes the first prompt hook in every IDE, so scans normally find a cache.
173
+ _sync_guardrail_config(ai_client, auth_info.tenant_id)
174
+
175
+
176
+ def _sync_guardrail_config(ai_client: 'AISecurityManagerClient', tenant_id: Optional[str]) -> None:
177
+ """Refresh the guardrail config cache when it is expired or belongs to another tenant.
178
+
179
+ Every step here swallows its own failures - a broken cache or an unreachable platform
180
+ must never fail the session.
181
+ """
182
+ cached = load_guardrail_config()
183
+ if cached is not None and not cached.needs_refresh(tenant_id):
184
+ logger.debug('Guardrail config cache is fresh, skipping fetch')
185
+ return
186
+
187
+ resolved = ai_client.get_resolved_guardrails()
188
+ if resolved:
189
+ save_guardrail_config(resolved, tenant_id)
190
+ logger.debug('Guardrail config cache updated')
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import os
3
+ import tempfile
3
4
  from functools import cache
4
5
  from typing import TYPE_CHECKING, AnyStr, Optional, Union
5
6
 
@@ -86,6 +87,32 @@ def get_file_content(file_path: Union[str, 'PathLike']) -> Optional[AnyStr]:
86
87
  logger.warn('Permission denied to read the file: %s', file_path)
87
88
 
88
89
 
90
+ def atomic_write_text(filename: str, content: str) -> None:
91
+ """Write via a temp file + rename so concurrent CLI processes never read a torn file."""
92
+ directory = os.path.dirname(filename)
93
+ file_descriptor, temp_filename = tempfile.mkstemp(dir=directory, prefix=f'.{os.path.basename(filename)}.')
94
+ try:
95
+ with os.fdopen(file_descriptor, 'w', encoding='UTF-8') as file:
96
+ file.write(content)
97
+ file.flush()
98
+ os.fsync(file.fileno())
99
+
100
+ os.replace(temp_filename, filename)
101
+ except Exception:
102
+ if os.path.exists(temp_filename):
103
+ os.remove(temp_filename)
104
+ raise
105
+
106
+
107
+ def quarantine_corrupt_file(filename: str) -> None:
108
+ # Renamed rather than deleted: the file may hold the only copy of the user's credentials,
109
+ # and keeping it around leaves something to look at in the next bug report.
110
+ try:
111
+ os.replace(filename, f'{filename}.corrupt')
112
+ except OSError as e:
113
+ logger.warning('Failed to quarantine corrupt file, %s', {'filename': filename}, exc_info=e)
114
+
115
+
89
116
  def load_json(txt: str) -> Optional[dict]:
90
117
  try:
91
118
  return json.loads(txt)
@@ -1,10 +1,10 @@
1
1
  import os
2
- import tempfile
3
2
  from collections.abc import Hashable
4
3
  from typing import Any, TextIO
5
4
 
6
5
  import yaml
7
6
 
7
+ from cycode.cli.utils.path_utils import atomic_write_text, quarantine_corrupt_file
8
8
  from cycode.logger import get_logger
9
9
 
10
10
  logger = get_logger('YAML Utils')
@@ -35,15 +35,6 @@ def _yaml_object_safe_load(file: TextIO) -> dict[Hashable, Any]:
35
35
  return loaded_file
36
36
 
37
37
 
38
- def _quarantine_corrupt_file(filename: str) -> None:
39
- # Renamed rather than deleted: the file may hold the only copy of the user's credentials,
40
- # and keeping it around leaves something to look at in the next bug report.
41
- try:
42
- os.replace(filename, f'{filename}.corrupt')
43
- except OSError as e:
44
- logger.warning('Failed to quarantine corrupt file, %s', {'filename': filename}, exc_info=e)
45
-
46
-
47
38
  def read_yaml_file(filename: str) -> dict[Hashable, Any]:
48
39
  if not os.access(filename, os.R_OK) or not os.path.exists(filename):
49
40
  logger.debug('Config file is not accessible or does not exist: %s', {'filename': filename})
@@ -54,7 +45,7 @@ def read_yaml_file(filename: str) -> dict[Hashable, Any]:
54
45
  return _yaml_object_safe_load(file)
55
46
  except yaml.YAMLError as e:
56
47
  logger.warning('Config file is corrupt and will be moved aside, %s', {'filename': filename}, exc_info=e)
57
- _quarantine_corrupt_file(filename)
48
+ quarantine_corrupt_file(filename)
58
49
  return {}
59
50
 
60
51
 
@@ -64,19 +55,7 @@ def write_yaml_file(filename: str, content: dict[Hashable, Any]) -> None:
64
55
  logger.warning('No write permission for file. Cannot save config, %s', {'filename': filename})
65
56
  return
66
57
 
67
- # Atomic write to avoid race conditions between concurrent CLI processes
68
- file_descriptor, temp_filename = tempfile.mkstemp(dir=directory, prefix=f'.{os.path.basename(filename)}.')
69
- try:
70
- with os.fdopen(file_descriptor, 'w', encoding='UTF-8') as file:
71
- yaml.safe_dump(content, file)
72
- file.flush()
73
- os.fsync(file.fileno())
74
-
75
- os.replace(temp_filename, filename)
76
- except Exception:
77
- if os.path.exists(temp_filename):
78
- os.remove(temp_filename)
79
- raise
58
+ atomic_write_text(filename, yaml.safe_dump(content))
80
59
 
81
60
 
82
61
  def update_yaml_file(filename: str, content: dict[Hashable, Any]) -> None:
@@ -18,6 +18,7 @@ class AISecurityManagerClient:
18
18
  _CONVERSATIONS_PATH = 'v4/ai-security/interactions/conversations'
19
19
  _EVENTS_PATH = 'v4/ai-security/interactions/events'
20
20
  _SESSION_CONTEXT_PATH = 'v4/ai-security/interactions/session-context'
21
+ _RESOLVED_GUARDRAILS_PATH = 'v4/ai-security/guardrails/resolved'
21
22
 
22
23
  def __init__(self, client: CycodeClientBase, service_config: 'AISecurityManagerServiceConfigBase') -> None:
23
24
  self.client = client
@@ -92,6 +93,15 @@ class AISecurityManagerClient:
92
93
  logger.debug('Failed to create AI hook event', exc_info=e)
93
94
  # Don't fail the hook if tracking fails
94
95
 
96
+ def get_resolved_guardrails(self) -> Optional[dict]:
97
+ """Fetch the tenant's resolved guardrail config (per-agent modes + sensitive-path globs)."""
98
+ try:
99
+ response = self.client.get(self._build_endpoint_path(self._RESOLVED_GUARDRAILS_PATH))
100
+ return response.json()
101
+ except Exception as e:
102
+ logger.debug('Failed to fetch resolved guardrail config', exc_info=e)
103
+ return None
104
+
95
105
  def report_session_context(
96
106
  self,
97
107
  hostname: Optional[str] = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.22.2.dev3
3
+ Version: 3.23.1.dev1
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
@@ -1,4 +1,4 @@
1
- cycode/__init__.py,sha256=Y1jHOAiaY2fAHhU4emWEFYVky7rvEWjAbscgjIcFwmM,396
1
+ cycode/__init__.py,sha256=9avEgdZkCaD_4Klbu5ohfcM_MidKc6b4_XrsOWD1zjU,396
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=AlR2durAEbsa47PDfIj7JtMvJDWA_Dq6wPtVuMJYSCs,10250
@@ -6,8 +6,8 @@ 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
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=U9lToL22ik3oX6bcZxT5de79Fu0eZA5D5p-q9daE5Iw,778
10
- cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=zLUtZJ3yFu9n0r71lhhx9gUUXclTwgCyU38ULbVB--0,9329
9
+ cycode/cli/apps/ai_guardrails/consts.py,sha256=CO3j7CiVoc0CuWtIjYEWmGSXNAd2DfiPhffW-gZMRT4,1152
10
+ cycode/cli/apps/ai_guardrails/hooks_manager.py,sha256=gIP4LGyOL1OFmCfxM1nrjedrBpCwLc_mcb7k40bIz_Q,9398
11
11
  cycode/cli/apps/ai_guardrails/ides/__init__.py,sha256=5odqWNfvvbFfAtR8ioEytXZ5VczWAOGyr1xuASC_OnQ,3346
12
12
  cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py,sha256=XPIc9pZFEgdGVTSSAl9yqI48F88GKlFnJ5FsLUhHjCE,3981
13
13
  cycode/cli/apps/ai_guardrails/ides/_skill_utils.py,sha256=Xk8z-uquTMvg4JxNwZxnclR79ME7ZKq7rxht3aFQUeo,3972
@@ -16,17 +16,18 @@ cycode/cli/apps/ai_guardrails/ides/claude_code.py,sha256=JCSB2PRPtZETFSskxV02hUw
16
16
  cycode/cli/apps/ai_guardrails/ides/codex.py,sha256=P3dCa2bHLoeKs10TN6OmwP6BjcuJkNMkfUFa4T7n9Vo,12335
17
17
  cycode/cli/apps/ai_guardrails/ides/copilot.py,sha256=XK2CLdB3oLJrP5Z-pGNsLbnwfehv4AW4vTLdvSE3eb0,21894
18
18
  cycode/cli/apps/ai_guardrails/ides/cursor.py,sha256=cqi12ELV5ZF7ANUA9n33gc8Hj9hURqEBLJ3gzf-v68w,5850
19
- cycode/cli/apps/ai_guardrails/install_command.py,sha256=3IV_MHpc7R-OJ2kUTDAtemkGZUQ9YAWX3lJBgTsi0zo,4190
19
+ cycode/cli/apps/ai_guardrails/install_command.py,sha256=3T-Wj_Fr871uhfTe77CGqrkeAwvHafK_aEv7BZ0jAJU,4069
20
20
  cycode/cli/apps/ai_guardrails/scan/__init__.py,sha256=qJc82XiQGiAuc1sYY8Ij_A-qXpxgLPuayQq8xWlouMA,48
21
- cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=drAslw6vW3kxmbCs2qPCUbUPR7PJouT2lsXtu5sD-lQ,1094
21
+ cycode/cli/apps/ai_guardrails/scan/consts.py,sha256=8H8JXlgm65sYkgTXqsBInHhHe80UPjqY4vumZXB4GGM,1060
22
22
  cycode/cli/apps/ai_guardrails/scan/detach.py,sha256=8BRgBq9Qi8nXEx6NKlE8scSRPkO30XCfsiI5PLvq6Gk,2700
23
- cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=wxDY2-bxc9MNVWWediQPpm635IiW0WSNaetrZOH39WQ,18823
23
+ cycode/cli/apps/ai_guardrails/scan/guardrail_config.py,sha256=QSBwmkTdlC9zXIqI3gIiOlefviJWrpeJHzFu1Cs-zgE,7051
24
+ cycode/cli/apps/ai_guardrails/scan/handlers.py,sha256=vOAvknrlCN-G_AUE7adlZAg0V7BhtGTYKB4r1yZDBu0,17695
24
25
  cycode/cli/apps/ai_guardrails/scan/payload.py,sha256=sWsWq5yXP54MVCajsb180kLuprI_M2kspMOzQGh7r_o,1534
25
- cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=BZoNNdDQ9tqnfwhB4X1-bDtudaOQc_gXizm3IVwa28o,3351
26
- cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=YpfGVmP05qjeoCr_6UVW3rO_MAJ6CJZDfrR1v2OJZ8Y,7465
26
+ cycode/cli/apps/ai_guardrails/scan/policy.py,sha256=3HuDoL_NYE3lyHRAlIMleq6pi-stIDQwotHsKhUFhTQ,5000
27
+ cycode/cli/apps/ai_guardrails/scan/scan_command.py,sha256=fgV32phgkBPwqSau7oFDISZ58CVpq214JlYwZQM9bnM,8318
27
28
  cycode/cli/apps/ai_guardrails/scan/types.py,sha256=ybQm242QN0l_4SSNX4xMHXxzqEK-MW-hfIOixI7zvGU,1497
28
29
  cycode/cli/apps/ai_guardrails/scan/utils.py,sha256=QzR_zmivDYwg2-F8g4bFfsycHhNo-pPuJly0P_l0gm8,2879
29
- cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=OxpqSROhN3NTKE3TiDTsKrYu8dXRrUDdwUEUzlRxnlQ,6215
30
+ cycode/cli/apps/ai_guardrails/session_start_command.py,sha256=oEW-OsHXkf8P7SX1vKa9Ddlv_hASRiQ42JZR3MnJfRg,7171
30
31
  cycode/cli/apps/ai_guardrails/status_command.py,sha256=Uqss68TEPCYPXpLix6Bh-4J3g-khxWsAqlIGYH5x4bQ,3203
31
32
  cycode/cli/apps/ai_guardrails/uninstall_command.py,sha256=dOmePfZmlHAPy2zEJM1yMtSuDqvzDwtqgmLKYK-T9PI,2698
32
33
  cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ6VNqG4aEFBAdY,553
@@ -182,7 +183,7 @@ cycode/cli/utils/git_proxy.py,sha256=FPHMBiyLFK9X9vKYpKySRKJH6Dc9Cb3nO241Q95dASE
182
183
  cycode/cli/utils/host_info.py,sha256=ba3scmnOJQOtNpVT-rf2-FKb0ocGzpPUONsG-uxmhro,6249
183
184
  cycode/cli/utils/ignore_utils.py,sha256=cODqhnOHA2kRo8rMY0YcmcKkmXNPOC9UTCmFu62RRqE,15567
184
185
  cycode/cli/utils/jwt_utils.py,sha256=EGI-0CKhCGY8hIcZ9b9diq9hqtOUf8Ha8ukeVJIf974,818
185
- cycode/cli/utils/path_utils.py,sha256=U5te1unzhs9pnU5d9BWExgFWElHQkgKvFxKiOF-lp-w,3245
186
+ cycode/cli/utils/path_utils.py,sha256=zc48CSU7hxqjSgfH6h5M1B0kIcKW44BOJrUa_6z-mAo,4317
186
187
  cycode/cli/utils/progress_bar.py,sha256=bKBWHHdZsVkdDdWMJLfgLGR0cBYeB44P_DpRM8pvWqU,9528
187
188
  cycode/cli/utils/scan_batch.py,sha256=5xKGVDVqoRxdKhuZkK11x4QrNqKmU20Q83E_fy8Nndk,5188
188
189
  cycode/cli/utils/scan_utils.py,sha256=sTj7j9dVHcgeMqfYp8sO78ZiWX8LnhpgjCOi1N1gmAM,2248
@@ -192,10 +193,10 @@ cycode/cli/utils/task_timer.py,sha256=wxfM2TtJGjc1F17CIja_Qmt6zd4a1qdMwuz0ltgTDA
192
193
  cycode/cli/utils/trust_store.py,sha256=nEVyaDHhBu2f1Lw1CeHogaD8lQIp6u-sxaVDEKEhIW8,1992
193
194
  cycode/cli/utils/url_utils.py,sha256=2ZhRCbQsKnvAl5MBugQ5CrwuALP8uivsCI6chlqxahM,2304
194
195
  cycode/cli/utils/version_checker.py,sha256=0f5PaTk02ZkDxzBqZOeMV9mU_CWcx6HKW80jUKFOOZs,8239
195
- cycode/cli/utils/yaml_utils.py,sha256=ty4FlwrM49OoYSMv8U6ZGlB0JRds8nxaijHQZebBVoU,2991
196
+ cycode/cli/utils/yaml_utils.py,sha256=eyOcfNtqvmRMKrKY5jCdwwKKOsXAFchcBAoES0F_o7c,2167
196
197
  cycode/config.py,sha256=jHORGZQcAXkAGSf2XreC-RQoc8sdNWja69QKtPWTbWo,1044
197
198
  cycode/cyclient/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
198
- cycode/cyclient/ai_security_manager_client.py,sha256=Ljv93RQvh0cy-dh84zGw7UnVdN2KPulmmSvrvFfXdNw,4939
199
+ cycode/cyclient/ai_security_manager_client.py,sha256=LXOLtA2Moa4bb0r1W9y6gQ9MLN3VPWhaNMGs1GILx9E,5450
199
200
  cycode/cyclient/ai_security_manager_service_config.py,sha256=83pQzgOb93JW6E-dznJkI4c0NEXmQRlx9YZKMmjVwp8,808
200
201
  cycode/cyclient/auth_client.py,sha256=TwbmZ358Ancf-Q-IZolvfljZ8691_6botsqd0R0PLPk,2105
201
202
  cycode/cyclient/base_token_auth_client.py,sha256=mn5580d7A8Z2_zcdFKIJk78ADK7mViwTcV-4QCpRCGo,4369
@@ -216,8 +217,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
216
217
  cycode/cyclient/scan_client.py,sha256=DqAZ7u6Z_cvw9A9RlLkAQUgLRwPCCAsUq5U9umt4F7Y,16955
217
218
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
218
219
  cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
219
- cycode-3.22.2.dev3.dist-info/METADATA,sha256=8JTX553WEOga3CDpNP6QDIVruDmUg_DphQFSQldBwaI,93687
220
- cycode-3.22.2.dev3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
221
- cycode-3.22.2.dev3.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
222
- cycode-3.22.2.dev3.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
223
- cycode-3.22.2.dev3.dist-info/RECORD,,
220
+ cycode-3.23.1.dev1.dist-info/METADATA,sha256=4vnnUjN1UZGDWdXBApe-U2DMEicw9nRjMpXBNMt-C5A,93687
221
+ cycode-3.23.1.dev1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
222
+ cycode-3.23.1.dev1.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
223
+ cycode-3.23.1.dev1.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
224
+ cycode-3.23.1.dev1.dist-info/RECORD,,