11"""Handle AI guardrails session start: auth, conversation creation, session context."""
22
3+ import hashlib
4+ import json
35import sys
6+ import time
7+ from pathlib import Path
48from typing import TYPE_CHECKING , Annotated , Optional
59
610import typer
711
8- from cycode .cli .apps .ai_guardrails .ides import DEFAULT_IDE_NAME , get_ide
9- from cycode .cli .apps .ai_guardrails .ides .base import IDE
12+ from cycode .cli .apps .ai_guardrails .ides import DEFAULT_IDE_NAME , collect_all_session_contexts , get_ide
1013from cycode .cli .apps .ai_guardrails .scan .utils import read_stdin_text , safe_json_parse
1114from cycode .cli .apps .auth .auth_common import get_authorization_info
1215from cycode .cli .apps .auth .auth_manager import AuthManager
2629
2730logger = get_logger ('AI Guardrails' )
2831
32+ _SESSION_CONTEXT_CACHE_FILE = '.session-context-cache'
33+ _SESSION_CONTEXT_TTL_SECONDS = 7 * 24 * 60 * 60
2934
30- def _report_session_context (ai_client : 'AISecurityManagerClient' , ide : IDE , user_email : Optional [str ]) -> None :
31- """Report IDE session context to the AI security manager. Never raises."""
35+
36+ def _session_context_cache_path () -> Path :
37+ return Path .home () / '.cycode' / _SESSION_CONTEXT_CACHE_FILE
38+
39+
40+ def _session_context_digest (report : dict ) -> str :
41+ """Deterministic hash of the outgoing payload (not the raw config files, which churn)."""
42+ canonical = json .dumps (report , sort_keys = True , separators = (',' , ':' ), default = str )
43+ return hashlib .sha256 (canonical .encode ('utf-8' )).hexdigest ()
44+
45+
46+ def _should_skip_report (digest : str , tenant_id : Optional [str ]) -> bool :
47+ """Skip when the same payload was already sent for this tenant and the TTL hasn't expired."""
3248 try :
33- global_config_file , enabled_plugins = ide .get_session_context ()
34- if not global_config_file and not enabled_plugins :
35- return
36- ai_client .report_session_context (
37- hostname = get_hostname (),
38- platform_name = get_platform_name (),
39- os_version = get_os_version (),
40- serial_number = get_serial_number (),
41- last_login_user = get_last_login_user (),
42- global_config_file = global_config_file ,
43- enabled_plugins = enabled_plugins ,
44- user_email = user_email ,
49+ cache = json .loads (_session_context_cache_path ().read_text (encoding = 'utf-8' ))
50+ return (
51+ cache .get ('hash' ) == digest
52+ and cache .get ('tenant_id' ) == tenant_id
53+ and time .time () - float (cache .get ('sent_at' , 0 )) < _SESSION_CONTEXT_TTL_SECONDS
54+ )
55+ except Exception :
56+ # Missing/corrupt cache reads as a miss - over-sending is harmless
57+ return False
58+
59+
60+ def _save_report_cache (digest : str , tenant_id : Optional [str ]) -> None :
61+ try :
62+ cache_path = _session_context_cache_path ()
63+ cache_path .parent .mkdir (parents = True , exist_ok = True )
64+ cache_path .write_text (
65+ json .dumps ({'hash' : digest , 'tenant_id' : tenant_id , 'sent_at' : time .time ()}), encoding = 'utf-8'
4566 )
67+ except Exception as e :
68+ logger .debug ('Failed to write session context cache' , exc_info = e )
69+
70+
71+ def _report_session_context (
72+ ai_client : 'AISecurityManagerClient' ,
73+ user_email : Optional [str ],
74+ tenant_id : Optional [str ],
75+ ) -> None :
76+ """Report the device + cross-IDE session context to the AI security manager. Never raises.
77+
78+ The device context is always reported. MCP configs are collected from every registered IDE,
79+ not just the triggering one. Unchanged payloads are skipped via a hash cache until the TTL expires.
80+ """
81+ try :
82+ config_files_by_ide , enabled_plugins = collect_all_session_contexts ()
83+ report = {
84+ 'hostname' : get_hostname (),
85+ 'platform_name' : get_platform_name (),
86+ 'os_version' : get_os_version (),
87+ 'serial_number' : get_serial_number (),
88+ 'last_login_user' : get_last_login_user (),
89+ 'config_files' : list (config_files_by_ide .values ()),
90+ 'enabled_plugins' : enabled_plugins ,
91+ 'user_email' : user_email ,
92+ }
93+
94+ digest = _session_context_digest (report )
95+ if _should_skip_report (digest , tenant_id ):
96+ logger .debug ('Session context unchanged; skipping report' )
97+ return
98+
99+ if ai_client .report_session_context (** report ):
100+ _save_report_cache (digest , tenant_id )
46101 except Exception as e :
47102 logger .debug ('Failed to report session context' , exc_info = e )
48103
@@ -61,7 +116,7 @@ def session_start_command(
61116 """Handle session start: ensure auth, create conversation, report session context."""
62117 ide_integration = get_ide (ide )
63118
64- # Step 1: Ensure authentication
119+ # Ensure authentication
65120 auth_info = get_authorization_info (ctx )
66121 if auth_info is None :
67122 logger .debug ('Not authenticated, starting authentication' )
@@ -70,10 +125,11 @@ def session_start_command(
70125 except Exception as err :
71126 handle_auth_exception (ctx , err )
72127 return
128+ auth_info = get_authorization_info (ctx )
73129 else :
74130 logger .debug ('Already authenticated' )
75131
76- # Step 2: Read stdin payload (backward compat: old hooks pipe no stdin)
132+ # Read stdin payload (backward compat: old hooks pipe no stdin)
77133 if sys .stdin .isatty ():
78134 logger .debug ('No stdin payload (TTY), skipping session initialization' )
79135 return
@@ -84,7 +140,7 @@ def session_start_command(
84140 logger .debug ('Empty or invalid stdin payload, skipping session initialization' )
85141 return
86142
87- # Step 3: Build session payload + initialize API client
143+ # Build session payload + initialize API client
88144 session_payload = ide_integration .build_session_payload (payload )
89145
90146 try :
@@ -93,11 +149,11 @@ def session_start_command(
93149 logger .debug ('Failed to initialize AI security client' , exc_info = e )
94150 return
95151
96- # Step 4: Create conversation
152+ # Create conversation
97153 try :
98154 ai_client .create_conversation (session_payload )
99155 except Exception as e :
100156 logger .debug ('Failed to create conversation during session start' , exc_info = e )
101157
102- # Step 5: Report session context (MCP servers, enabled plugins)
103- _report_session_context (ai_client , ide_integration , session_payload .ide_user_email )
158+ # Report session context (device + cross-IDE MCP servers and plugins)
159+ _report_session_context (ai_client , session_payload .ide_user_email , auth_info . tenant_id )
0 commit comments