@@ -154,8 +154,77 @@ def set_script_state(self, id: str, new_state: ExecutionState):
154154
155155
156156script_store = ScriptStore ()
157+ MAX_CONSECUTIVE_MALICIOUS_ACTIONS = 3
158+ MAX_TOTAL_MALICIOUS_ACTIONS = 4
157159
158160
161+ class BehaviorRecord :
162+ """
163+ Tracks gatekeeper verdicts for a single session to detect malicious behavior.
164+
165+ A session is flagged as security compromised if it accumulates MAX_TOTAL_MALICIOUS_ACTIONS
166+ total malicious actions or MAX_CONSECUTIVE_MALICIOUS_ACTIONS consecutive malicious actions.
167+ Once flagged, the session is permanently marked and all subsequent scripts require confirmation.
168+ """
169+
170+ def __init__ (self ):
171+ self ._consecutive_malicious_action_counts = 0
172+ self ._total_malicious_action_counts = 0
173+ self ._previous_action_status = None
174+ self ._current_action_status = None
175+ self ._is_security_compromised : bool = False
176+
177+ def add_record (self , status : GatekeeperStatus ):
178+ """
179+ Record a gatekeeper verdict and update the security compromised flag if
180+ thresholds are met.
181+ """
182+ # No need to update the record if it's already considered security compromised
183+ if self ._is_security_compromised :
184+ return
185+
186+ self ._previous_action_status = self ._current_action_status
187+ self ._current_action_status = status
188+
189+ if status == GatekeeperStatus .MALICIOUS :
190+ self ._consecutive_malicious_action_counts += 1
191+ self ._total_malicious_action_counts += 1
192+ else :
193+ self ._consecutive_malicious_action_counts = 0
194+
195+ # Check if the record matches the conditions of being considered as malicious
196+ if self ._total_malicious_action_counts >= MAX_TOTAL_MALICIOUS_ACTIONS :
197+ self ._is_security_compromised = True
198+ return
199+
200+ if self ._consecutive_malicious_action_counts >= MAX_CONSECUTIVE_MALICIOUS_ACTIONS :
201+ self ._is_security_compromised = True
202+
203+ @property
204+ def is_security_compromised (self ) -> bool :
205+ return self ._is_security_compromised
206+
207+ @property
208+ def is_previous_action_malicious (self ) -> bool :
209+ return self ._previous_action_status == GatekeeperStatus .MALICIOUS
210+
211+
212+ class BehaviorRecordManager :
213+ """Manages per-session BehaviorRecords, creating them on first access."""
214+
215+ def __init__ (self ):
216+ self ._records : dict [str , BehaviorRecord ] = dict ()
217+
218+ def get_record_by_session_id (self , session_id : str ) -> BehaviorRecord :
219+ """Return the BehaviorRecord for a session, creating one if it doesn't exist."""
220+ if session_id not in self ._records :
221+ self ._records [session_id ] = BehaviorRecord ()
222+
223+ return self ._records [session_id ]
224+
225+
226+ behavior_record_manager = BehaviorRecordManager ()
227+
159228BASH_STRICT_PREAMBLE = "set -euo pipefail; "
160229
161230SYSTEMD_RUN_ARGS = [
@@ -209,6 +278,7 @@ class RunScriptInteractiveResult(BaseModel):
209278 id : str
210279 status : GatekeeperStatus
211280 detail : str
281+ is_security_compromised : bool
212282
213283
214284# class UserInfo(BaseModel):
@@ -314,9 +384,16 @@ async def run_script_interactive(
314384 host : Host = None ,
315385) -> ToolResult :
316386 script_details = script_store .get_script_details (token )
387+ behavior_record = behavior_record_manager .get_record_by_session_id (ctx .session_id )
388+
389+ needs_confirmation = (
390+ script_details .needs_confirmation
391+ or behavior_record .is_security_compromised
392+ or behavior_record .is_previous_action_malicious
393+ )
317394
318395 # Verify that this script requires confirmation
319- if not script_details . needs_confirmation :
396+ if not needs_confirmation :
320397 raise ToolError ("This script does not require confirmation. Use run_script instead of run_script_interactive." )
321398
322399 # Check if the passed parameters match the stored script details
@@ -338,6 +415,8 @@ async def run_script_interactive(
338415 (BASH_STRICT_PREAMBLE + script ) if script_type == SCRIPT_TYPE_BASH else script ,
339416 readonly = readonly ,
340417 )
418+ behavior_record .add_record (gatekeeper_result .status )
419+
341420 if gatekeeper_result .status != GatekeeperStatus .OK :
342421 script_store .set_script_state (token , "rejected-gatekeeper" )
343422 raise ToolError (gatekeeper_result .description )
@@ -352,7 +431,12 @@ async def run_script_interactive(
352431 )
353432 ]
354433
355- structured_content_obj = RunScriptInteractiveResult (id = result_id , status = GatekeeperStatus .OK , detail = "" )
434+ structured_content_obj = RunScriptInteractiveResult (
435+ id = result_id ,
436+ status = GatekeeperStatus .OK ,
437+ detail = "" ,
438+ is_security_compromised = behavior_record .is_security_compromised ,
439+ )
356440
357441 return ToolResult (content = content , structured_content = structured_content_obj .model_dump ())
358442
@@ -413,23 +497,32 @@ async def validate_script(
413497 readonly = readonly ,
414498 )
415499
500+ behavior_record = behavior_record_manager .get_record_by_session_id (ctx .session_id )
501+ behavior_record .add_record (gatekeeper_result .status )
502+
416503 id = script_store .add_script (description , script , script_type , host , readonly )
417504 script_details = script_store .get_script_details (id )
418505
419506 if gatekeeper_result .status != GatekeeperStatus .OK :
420507 script_store .set_script_state (id , "rejected-gatekeeper" )
421508 raise ToolError (gatekeeper_result .description )
422509
510+ needs_confirmation = (
511+ script_details .needs_confirmation
512+ or behavior_record .is_security_compromised
513+ or behavior_record .is_previous_action_malicious
514+ )
515+
423516 result = ToolResult (
424517 content = [
425518 TextContent (
426519 type = "text" ,
427- text = f"Script passed gatekeeper validation and is stored with ID { id } . Please use { _pick_execution_tool (script_details . needs_confirmation )} to execute the validated script." ,
520+ text = f"Script passed gatekeeper validation and is stored with ID { id } . Please use { _pick_execution_tool (needs_confirmation )} to execute the validated script." ,
428521 )
429522 ],
430523 structured_content = {
431524 "token" : id ,
432- "needs_confirmation" : script_details . needs_confirmation ,
525+ "needs_confirmation" : needs_confirmation ,
433526 },
434527 )
435528 return result
@@ -448,10 +541,17 @@ async def run_script(
448541 token : t .Annotated [str , Field (description = "The token returned by the validate_script tool." )],
449542) -> str :
450543 script_details = script_store .get_script_details (token )
544+ behavior_record = behavior_record_manager .get_record_by_session_id (ctx .session_id )
545+
546+ needs_confirmation = (
547+ script_details .needs_confirmation
548+ or behavior_record .is_security_compromised
549+ or behavior_record .is_previous_action_malicious
550+ )
451551
452552 # Verify that this script doesn't require confirmation
453- if script_details . needs_confirmation :
454- raise ToolError (f "This script requires confirmation. Use { _pick_execution_tool ( True ) } instead of run_script." )
553+ if needs_confirmation :
554+ raise ToolError ("This script requires confirmation. Use run_script_with_confirmation instead of run_script." )
455555
456556 script_store .set_script_state (token , "executing" )
457557 try :
@@ -498,9 +598,16 @@ async def run_script_with_confirmation(
498598 host : Host = None ,
499599) -> str :
500600 script_details = script_store .get_script_details (token )
601+ behavior_record = behavior_record_manager .get_record_by_session_id (ctx .session_id )
602+
603+ needs_confirmation = (
604+ script_details .needs_confirmation
605+ or behavior_record .is_security_compromised
606+ or behavior_record .is_previous_action_malicious
607+ )
501608
502609 # Verify that this script requires confirmation
503- if not script_details . needs_confirmation :
610+ if not needs_confirmation :
504611 raise ToolError (
505612 "This script does not require confirmation. Use run_script instead of run_script_with_confirmation."
506613 )
@@ -527,6 +634,8 @@ async def run_script_with_confirmation(
527634 (BASH_STRICT_PREAMBLE + script ) if script_type == SCRIPT_TYPE_BASH else script ,
528635 readonly = readonly ,
529636 )
637+ behavior_record .add_record (gatekeeper_result .status )
638+
530639 if gatekeeper_result .status != GatekeeperStatus .OK :
531640 script_store .set_script_state (token , "rejected-gatekeeper" )
532641 raise ToolError (gatekeeper_result .description )
0 commit comments