Skip to content

Commit f7a2b30

Browse files
committed
Merge branch 'CM-58248-deny-prompts-if-not-authenticated' into CM-58022-cycode-guardrails-support-cursor-scan-via-hooks
2 parents d183e2e + 29114c2 commit f7a2b30

File tree

2 files changed

+57
-33
lines changed

2 files changed

+57
-33
lines changed

cycode/cli/apps/scan/prompt/prompt_command.py

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sys
1313
from typing import Annotated
1414

15+
import click
1516
import typer
1617

1718
from cycode.cli.apps.scan.prompt.handlers import get_handler_for_event
@@ -20,13 +21,44 @@
2021
from cycode.cli.apps.scan.prompt.response_builders import get_response_builder
2122
from cycode.cli.apps.scan.prompt.types import AiHookEventType
2223
from cycode.cli.apps.scan.prompt.utils import output_json, safe_json_parse
24+
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError
2325
from cycode.cli.utils.get_api_client import get_ai_security_manager_client, get_scan_cycode_client
2426
from cycode.cli.utils.sentry import add_breadcrumb
2527
from cycode.logger import get_logger
2628

2729
logger = get_logger('AI Guardrails')
2830

2931

32+
def _get_auth_error_message(error: Exception) -> str:
33+
"""Get user-friendly message for authentication errors."""
34+
if isinstance(error, click.ClickException):
35+
# Missing credentials
36+
return f'{error.message} Please run `cycode configure` to set up your credentials.'
37+
38+
if isinstance(error, HttpUnauthorizedError):
39+
# Invalid/expired credentials
40+
return (
41+
'Unable to authenticate to Cycode. Your credentials are invalid or have expired. '
42+
'Please run `cycode configure` to update your credentials.'
43+
)
44+
45+
# Fallback
46+
return 'Authentication failed. Please run `cycode configure` to set up your credentials.'
47+
48+
49+
def _initialize_clients(ctx: typer.Context) -> None:
50+
"""Initialize API clients.
51+
52+
May raise click.ClickException if credentials are missing,
53+
or HttpUnauthorizedError if credentials are invalid.
54+
"""
55+
scan_client = get_scan_cycode_client(ctx)
56+
ctx.obj['client'] = scan_client
57+
58+
ai_security_client = get_ai_security_manager_client(ctx)
59+
ctx.obj['ai_security_client'] = ai_security_client
60+
61+
3062
def prompt_command(
3163
ctx: typer.Context,
3264
ide: Annotated[
@@ -52,7 +84,6 @@ def prompt_command(
5284
"""
5385
add_breadcrumb('prompt')
5486

55-
# Read JSON payload from stdin
5687
stdin_data = sys.stdin.read().strip()
5788
payload = safe_json_parse(stdin_data)
5889

@@ -64,50 +95,39 @@ def prompt_command(
6495
output_json(response_builder.allow_prompt())
6596
return
6697

67-
# Create unified payload object
6898
unified_payload = AIHookPayload.from_payload(payload, tool=tool)
69-
70-
# Extract event type from unified payload
7199
event_name = unified_payload.event_name
72100
logger.debug('Processing AI guardrails hook', extra={'event_name': event_name, 'tool': tool})
73101

74-
scan_client = get_scan_cycode_client(ctx)
75-
ctx.obj['client'] = scan_client
76-
77-
ai_security_client = get_ai_security_manager_client(ctx)
78-
ctx.obj['ai_security_client'] = ai_security_client
79-
80-
# Load policy (merges defaults <- user config <- repo config)
81-
# Extract first workspace root from payload if available
82102
workspace_roots = payload.get('workspace_roots', ['.'])
83103
policy = load_policy(workspace_roots[0])
84104

85-
# Get the appropriate handler for this event
86-
handler = get_handler_for_event(event_name)
105+
try:
106+
_initialize_clients(ctx)
87107

88-
if handler is None:
89-
logger.debug('Unknown hook event, allowing by default', extra={'event_name': event_name})
90-
# Unknown event type - allow by default
91-
output_json(response_builder.allow_prompt())
92-
return
108+
handler = get_handler_for_event(event_name)
109+
if handler is None:
110+
logger.debug('Unknown hook event, allowing by default', extra={'event_name': event_name})
111+
output_json(response_builder.allow_prompt())
112+
return
93113

94-
# Execute the handler and output the response
95-
try:
96114
response = handler(ctx, unified_payload, policy)
97115
logger.debug('Hook handler completed', extra={'event_name': event_name, 'response': response})
98116
output_json(response)
117+
118+
except (click.ClickException, HttpUnauthorizedError) as e:
119+
error_message = _get_auth_error_message(e)
120+
if event_name == AiHookEventType.PROMPT:
121+
output_json(response_builder.deny_prompt(error_message))
122+
return
123+
output_json(response_builder.deny_permission(error_message, 'Authentication required'))
124+
99125
except Exception as e:
100126
logger.error('Hook handler failed', exc_info=e)
101-
# Fail open by default
102127
if policy.get('fail_open', True):
103128
output_json(response_builder.allow_prompt())
104-
else:
105-
# Fail closed
106-
if event_name == AiHookEventType.PROMPT:
107-
output_json(
108-
response_builder.deny_prompt('Cycode guardrails error - blocking due to fail-closed policy')
109-
)
110-
else:
111-
output_json(
112-
response_builder.deny_permission('Cycode guardrails error', 'Blocking due to fail-closed policy')
113-
)
129+
return
130+
if event_name == AiHookEventType.PROMPT:
131+
output_json(response_builder.deny_prompt('Cycode guardrails error - blocking due to fail-closed policy'))
132+
return
133+
output_json(response_builder.deny_permission('Cycode guardrails error', 'Blocking due to fail-closed policy'))

cycode/cyclient/ai_security_manager_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from typing import TYPE_CHECKING, Optional
44

5+
from cycode.cli.exceptions.custom_exceptions import HttpUnauthorizedError
56
from cycode.cyclient.cycode_client_base import CycodeClientBase
67
from cycode.cyclient.logger import logger
78

@@ -44,9 +45,12 @@ def create_conversation(self, payload: 'AIHookPayload') -> Optional[str]:
4445

4546
try:
4647
self.client.post(self._build_endpoint_path(self._CONVERSATIONS_PATH), body=body)
48+
except HttpUnauthorizedError:
49+
# Authentication error - re-raise so prompt_command can catch it
50+
raise
4751
except Exception as e:
4852
logger.debug('Failed to create conversation', exc_info=e)
49-
# Don't fail the hook if tracking fails
53+
# Don't fail the hook if tracking fails (non-auth errors)
5054

5155
return conversation_id
5256

0 commit comments

Comments
 (0)