1818import os
1919import json
2020import getpass
21- from typing import Dict , Any , List
21+ from typing import Dict , Any , List , Optional
2222from contextlib import _AsyncGeneratorContextManager , asynccontextmanager
2323from langchain_ollama import ChatOllama
2424from langchain_openai import ChatOpenAI
2525from langchain_anthropic import ChatAnthropic
2626from langchain .agents import create_agent
27+ from langchain .agents .middleware import HumanInTheLoopMiddleware
2728from langchain_mcp_adapters .client import MultiServerMCPClient
2829
2930# Import MCP tools from the SDK package
@@ -133,6 +134,38 @@ def get_model(model_id: str = "gpt-oss:20b", temperature: float = 0.3):
133134 else :
134135 return ChatOllama (model = model_id , temperature = temperature )
135136
137+ # -----------------------------------------------------------------------------
138+ # Tool Metadata Helper Functions
139+ # -----------------------------------------------------------------------------
140+
141+ def _get_non_readonly_tools (tools : List [Any ]) -> List [str ]:
142+ """
143+ Extract tool names that have readOnly: false in their metadata.
144+
145+ This function checks the tool metadata for the 'readOnlyHint' annotation
146+ (which corresponds to the security.readOnly field in YAML) and returns
147+ a list of tool names that are marked as non-readonly (write operations).
148+
149+ Args:
150+ tools: List of LangChain tool objects with metadata
151+
152+ Returns:
153+ List of tool names that require human approval (readOnly: false)
154+ """
155+ non_readonly_tools = []
156+
157+ for tool in tools :
158+ # Check if tool has metadata
159+ if hasattr (tool , 'metadata' ) and tool .metadata :
160+ # Check readOnlyHint annotation (corresponds to security.readOnly in YAML)
161+ read_only_hint = tool .metadata .get ('readOnlyHint' , True )
162+
163+ # If readOnlyHint is False, this is a write operation that needs approval
164+ if read_only_hint is False :
165+ non_readonly_tools .append (tool .name )
166+
167+ return non_readonly_tools
168+
136169# -----------------------------------------------------------------------------
137170# Agent Creation Functions
138171# -----------------------------------------------------------------------------
@@ -370,6 +403,116 @@ async def agent_session():
370403
371404 return agent_session ()
372405
406+ async def create_security_ops_agent (
407+ model_id : str = "gpt-oss:20b" ,
408+ mcp_url : str = DEFAULT_MCP_URL ,
409+ transport : str = DEFAULT_TRANSPORT ,
410+ category : Optional [str ] = None ,
411+ enable_human_in_loop : bool = True ,
412+ ** kwargs
413+ ):
414+ """
415+ Create IBM i Security Operations Agent.
416+
417+ Args:
418+ model_id: Model identifier (default: "gpt-oss:20b")
419+ mcp_url: MCP server URL
420+ transport: Transport type
421+ category: Optional category filter for security tools. Options:
422+ - "vulnerability-assessment": Tools for identifying security vulnerabilities
423+ - "audit": Tools for auditing security configurations
424+ - "remediation": Tools for generating and executing security fixes
425+ - "user-management": Tools for managing user capabilities and permissions
426+ - None: Load all security tools (default)
427+ enable_human_in_loop: Enable human-in-the-loop middleware for non-readonly tools (default: True)
428+ **kwargs: Additional agent configuration options
429+
430+ Returns an async context manager that yields (agent, session).
431+ Usage: async with (await create_security_ops_agent()) as (agent, session): ...
432+ """
433+ client = get_mcp_client (mcp_url , transport )
434+
435+ @asynccontextmanager
436+ async def agent_session ():
437+ async with client .session ("ibmi_tools" ) as session :
438+ # Load security tools with optional category filtering
439+ if category :
440+ # Use annotation filtering to load tools by domain and category
441+ tools = await load_filtered_mcp_tools (
442+ session ,
443+ annotation_filters = {
444+ "domain" : "security" ,
445+ "category" : category
446+ },
447+ debug = True
448+ )
449+ print (f"✅ Loaded { len (tools )} security operations tools (category: { category } ) for Security Ops Agent" )
450+ else :
451+ # Load all security tools by domain
452+ tools = await load_filtered_mcp_tools (
453+ session ,
454+ annotation_filters = {"domain" : "security" },
455+ debug = True
456+ )
457+ print (f"✅ Loaded { len (tools )} security operations tools for Security Ops Agent" )
458+
459+ # Build human-in-the-loop middleware dynamically based on tool annotations
460+ middleware = []
461+ if enable_human_in_loop :
462+ non_readonly_tools = _get_non_readonly_tools (tools )
463+ if non_readonly_tools :
464+ interrupt_config = {}
465+ for tool_name in non_readonly_tools :
466+ interrupt_config [tool_name ] = {
467+ "allowed_decision" : ["approve" , "reject" ],
468+ }
469+
470+ middleware .append (HumanInTheLoopMiddleware (interrupt_on = interrupt_config ))
471+ print (f"🔒 Human-in-the-loop enabled for { len (non_readonly_tools )} non-readonly tools:" )
472+ for tool_name in non_readonly_tools :
473+ print (f" - { tool_name } " )
474+
475+ system_message = """You are a specialized IBM i security operations assistant.
476+ You help administrators identify security vulnerabilities, audit system configurations, and remediate security issues.
477+ Your role is to:
478+ - Identify security vulnerabilities and misconfigurations
479+ - Assess user privileges and special authorities
480+ - Audit file and object permissions for *PUBLIC access
481+ - Detect potential attack vectors (triggers, impersonation, privilege escalation)
482+ - Generate remediation commands for security lockdown
483+ - Explain security risks in business terms
484+ - Provide actionable recommendations for hardening system security
485+
486+ IMPORTANT SECURITY NOTES:
487+ - Always explain the security implications of findings
488+ - Distinguish between read-only assessment tools and destructive remediation tools
489+ - For remediation tools, you will be prompted for approval before execution
490+ - Recommend testing remediation commands in development before production
491+ - Prioritize findings by severity (critical vulnerabilities first)
492+
493+ Focus on helping administrators understand their security posture and take appropriate action to protect their IBM i systems."""
494+
495+ llm = get_model (model_id )
496+
497+ # Only pass middleware if it's not empty
498+ agent_kwargs = {
499+ "model" : llm ,
500+ "tools" : tools ,
501+ "system_prompt" : system_message ,
502+ "checkpointer" : get_shared_checkpointer (),
503+ "store" : get_shared_store (),
504+ "name" : "IBM i Security Operations" ,
505+ ** kwargs
506+ }
507+
508+ if middleware :
509+ agent_kwargs ["middleware" ] = middleware
510+
511+ agent = create_agent (** agent_kwargs )
512+ yield agent , session
513+
514+ return agent_session ()
515+
373516# -----------------------------------------------------------------------------
374517# Agent Registry and Factory Pattern
375518# -----------------------------------------------------------------------------
@@ -379,6 +522,7 @@ async def agent_session():
379522 "discovery" : create_sysadmin_discovery_agent ,
380523 "browse" : create_sysadmin_browse_agent ,
381524 "search" : create_sysadmin_search_agent ,
525+ "security" : create_security_ops_agent ,
382526}
383527
384528async def create_ibmi_agent (agent_type : str , ** kwargs ) -> _AsyncGeneratorContextManager [tuple [Any , Any ], None ]:
@@ -404,7 +548,8 @@ def list_available_agents() -> Dict[str, str]:
404548 "performance" : "System performance monitoring and analysis" ,
405549 "discovery" : "High-level system discovery and summarization" ,
406550 "browse" : "Detailed system browsing and exploration" ,
407- "search" : "System search and lookup capabilities"
551+ "search" : "System search and lookup capabilities" ,
552+ "security" : "Security vulnerability assessment and remediation"
408553 }
409554
410555def set_verbose_logging (enabled : bool ):
@@ -609,7 +754,7 @@ async def test_agents():
609754 "gpt-oss:20b" , # Default Ollama model
610755 "ollama:llama3.1" , # Explicit Ollama model
611756 "openai:gpt-4o" , # OpenAI model
612- "anthropic :claude-3. 7-sonnet" # Anthropic model
757+ "aanthropic :claude-3- 7-sonnet-20250219 " # Anthropic model
613758 ]
614759
615760 print ("Available model options:" )
0 commit comments