-
Notifications
You must be signed in to change notification settings - Fork 62
CM-62381-add-session-start-hook #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
ec67864
83cf72c
6396761
fb7420d
1e5e615
68afd59
2de9366
0bb5492
505d742
e2319fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Reader for ~/.cursor/mcp.json configuration file. | ||
|
|
||
| Extracts MCP server definitions from the Cursor global config file | ||
| for use in AI guardrails session-context reporting. | ||
| """ | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
| from cycode.logger import get_logger | ||
|
|
||
| logger = get_logger('AI Guardrails Cursor Config') | ||
|
|
||
| _CURSOR_MCP_CONFIG_PATH = Path.home() / '.cursor' / 'mcp.json' | ||
|
|
||
|
|
||
| def load_cursor_config(config_path: Optional[Path] = None) -> Optional[dict]: | ||
| """Load and parse ~/.cursor/mcp.json. | ||
|
|
||
| Args: | ||
| config_path: Override path for testing. Defaults to ~/.cursor/mcp.json. | ||
|
|
||
| Returns: | ||
| Parsed dict or None if file is missing or invalid. | ||
| """ | ||
| path = config_path or _CURSOR_MCP_CONFIG_PATH | ||
| if not path.exists(): | ||
| logger.debug('Cursor MCP config file not found', extra={'path': str(path)}) | ||
| return None | ||
| try: | ||
| content = path.read_text(encoding='utf-8') | ||
| return json.loads(content) | ||
| except Exception as e: | ||
| logger.debug('Failed to load Cursor MCP config file', exc_info=e) | ||
| return None |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import sys | ||
| from typing import Annotated | ||
|
|
||
| import typer | ||
|
|
||
| from cycode.cli.apps.ai_guardrails.consts import AIIDEType | ||
| from cycode.cli.apps.ai_guardrails.scan.claude_config import ( | ||
| get_enabled_plugins, | ||
| get_mcp_servers, | ||
| get_user_email, | ||
| load_claude_config, | ||
| load_claude_settings, | ||
| ) | ||
| from cycode.cli.apps.ai_guardrails.scan.cursor_config import load_cursor_config | ||
| from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload, _extract_from_claude_transcript | ||
|
RoniCycode marked this conversation as resolved.
Outdated
|
||
| from cycode.cli.apps.ai_guardrails.scan.utils import safe_json_parse | ||
| from cycode.cli.apps.auth.auth_common import get_authorization_info | ||
| from cycode.cli.apps.auth.auth_manager import AuthManager | ||
| from cycode.cli.exceptions.handle_auth_errors import handle_auth_exception | ||
| from cycode.cli.utils.get_api_client import get_ai_security_manager_client | ||
| from cycode.logger import get_logger | ||
|
|
||
| logger = get_logger('AI Guardrails') | ||
|
|
||
|
|
||
| def _build_session_payload(payload: dict, ide: str) -> AIHookPayload: | ||
| """Build an AIHookPayload from a session-start stdin payload.""" | ||
| if ide == AIIDEType.CLAUDE_CODE: | ||
| claude_config = load_claude_config() | ||
| ide_user_email = get_user_email(claude_config) if claude_config else None | ||
| ide_version, _, _ = _extract_from_claude_transcript(payload.get('transcript_path')) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have a better way to get claude version? i want to get rid of this function and also stop creating conversations for each hook |
||
|
|
||
| return AIHookPayload( | ||
| conversation_id=payload.get('session_id'), | ||
| ide_user_email=ide_user_email, | ||
| model=payload.get('model'), | ||
| ide_provider=AIIDEType.CLAUDE_CODE.value, | ||
| ide_version=ide_version, | ||
| ) | ||
|
|
||
| # Cursor | ||
| return AIHookPayload( | ||
| conversation_id=payload.get('conversation_id'), | ||
| ide_user_email=payload.get('user_email'), | ||
| model=payload.get('model'), | ||
| ide_provider=AIIDEType.CURSOR.value, | ||
| ide_version=payload.get('cursor_version'), | ||
| ) | ||
|
|
||
|
|
||
| def _get_mcp_servers_for_ide(ide: str) -> dict: | ||
| """Return configured MCP servers for the given IDE, or empty dict.""" | ||
| if ide == AIIDEType.CLAUDE_CODE: | ||
| config = load_claude_config() | ||
| elif ide == AIIDEType.CURSOR: | ||
| config = load_cursor_config() | ||
| else: | ||
| return {} | ||
| return get_mcp_servers(config) or {} if config else {} | ||
|
|
||
|
|
||
| def _get_enabled_plugins_for_ide(ide: str) -> dict: | ||
| """Return enabled plugins for the given IDE, or empty dict.""" | ||
| if ide == AIIDEType.CLAUDE_CODE: | ||
| settings = load_claude_settings() | ||
| return get_enabled_plugins(settings) or {} if settings else {} | ||
| return {} | ||
|
|
||
|
|
||
| def _report_session_context(ai_client, ide: str) -> None: | ||
|
Check failure on line 70 in cycode/cli/apps/ai_guardrails/session_start_command.py
|
||
| """Report IDE session context to the AI security manager. Never raises.""" | ||
|
RoniCycode marked this conversation as resolved.
|
||
| mcp_servers = _get_mcp_servers_for_ide(ide) | ||
| enabled_plugins = _get_enabled_plugins_for_ide(ide) | ||
| if not mcp_servers and not enabled_plugins: | ||
| return | ||
| ai_client.report_session_context(mcp_servers=mcp_servers, enabled_plugins=enabled_plugins) | ||
|
|
||
|
|
||
| def session_start_command( | ||
| ctx: typer.Context, | ||
| ide: Annotated[ | ||
| str, | ||
| typer.Option( | ||
| '--ide', | ||
| help='IDE that triggered the session start.', | ||
| hidden=True, | ||
| ), | ||
| ] = AIIDEType.CURSOR.value, | ||
| ) -> None: | ||
| """Handle session start: ensure auth, create conversation, report session context.""" | ||
| # Step 1: Ensure authentication | ||
| auth_info = get_authorization_info(ctx) | ||
| if auth_info is None: | ||
| logger.debug('Not authenticated, starting authentication') | ||
| try: | ||
| auth_manager = AuthManager() | ||
| auth_manager.authenticate() | ||
| except Exception as err: | ||
| handle_auth_exception(ctx, err) | ||
| return | ||
| else: | ||
| logger.debug('Already authenticated') | ||
|
|
||
| # Step 2: Read stdin payload (backward compat: old hooks pipe no stdin) | ||
| if sys.stdin.isatty(): | ||
| logger.debug('No stdin payload (TTY), skipping session initialization') | ||
| return | ||
|
|
||
|
RoniCycode marked this conversation as resolved.
|
||
| stdin_data = sys.stdin.read().strip() | ||
| payload = safe_json_parse(stdin_data) | ||
| if not payload: | ||
| logger.debug('Empty or invalid stdin payload, skipping session initialization') | ||
| return | ||
|
|
||
| # Step 3: Build session payload and initialize API client | ||
| session_payload = _build_session_payload(payload, ide) | ||
|
|
||
| try: | ||
| ai_client = get_ai_security_manager_client(ctx) | ||
| except Exception as e: | ||
| logger.debug('Failed to initialize AI security client', exc_info=e) | ||
| return | ||
|
|
||
| # Step 4: Create conversation | ||
| try: | ||
| ai_client.create_conversation(session_payload) | ||
| except Exception as e: | ||
| logger.debug('Failed to create conversation during session start', exc_info=e) | ||
|
|
||
| # Step 5: Report session context (MCP servers) | ||
| _report_session_context(ai_client, ide) | ||
Uh oh!
There was an error while loading. Please reload this page.