ag2 0.9.9__py3-none-any.whl → 0.9.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ag2 might be problematic. Click here for more details.

Files changed (88) hide show
  1. {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/METADATA +232 -210
  2. {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/RECORD +88 -80
  3. autogen/_website/generate_mkdocs.py +3 -3
  4. autogen/_website/notebook_processor.py +1 -1
  5. autogen/_website/utils.py +1 -1
  6. autogen/agentchat/assistant_agent.py +15 -15
  7. autogen/agentchat/chat.py +52 -40
  8. autogen/agentchat/contrib/agent_eval/criterion.py +1 -1
  9. autogen/agentchat/contrib/capabilities/text_compressors.py +5 -5
  10. autogen/agentchat/contrib/capabilities/tools_capability.py +1 -1
  11. autogen/agentchat/contrib/capabilities/transforms.py +1 -1
  12. autogen/agentchat/contrib/captainagent/agent_builder.py +1 -1
  13. autogen/agentchat/contrib/captainagent/captainagent.py +20 -19
  14. autogen/agentchat/contrib/graph_rag/falkor_graph_query_engine.py +2 -5
  15. autogen/agentchat/contrib/graph_rag/graph_rag_capability.py +5 -5
  16. autogen/agentchat/contrib/graph_rag/neo4j_graph_query_engine.py +18 -17
  17. autogen/agentchat/contrib/rag/mongodb_query_engine.py +2 -2
  18. autogen/agentchat/contrib/rag/query_engine.py +11 -11
  19. autogen/agentchat/contrib/retrieve_assistant_agent.py +3 -0
  20. autogen/agentchat/contrib/swarm_agent.py +3 -2
  21. autogen/agentchat/contrib/vectordb/couchbase.py +1 -1
  22. autogen/agentchat/contrib/vectordb/mongodb.py +1 -1
  23. autogen/agentchat/contrib/web_surfer.py +1 -1
  24. autogen/agentchat/conversable_agent.py +184 -80
  25. autogen/agentchat/group/context_expression.py +21 -21
  26. autogen/agentchat/group/handoffs.py +11 -11
  27. autogen/agentchat/group/multi_agent_chat.py +3 -2
  28. autogen/agentchat/group/on_condition.py +11 -11
  29. autogen/agentchat/group/safeguards/__init__.py +21 -0
  30. autogen/agentchat/group/safeguards/api.py +224 -0
  31. autogen/agentchat/group/safeguards/enforcer.py +1064 -0
  32. autogen/agentchat/group/safeguards/events.py +119 -0
  33. autogen/agentchat/group/safeguards/validator.py +435 -0
  34. autogen/agentchat/groupchat.py +58 -17
  35. autogen/agentchat/realtime/experimental/clients/realtime_client.py +2 -2
  36. autogen/agentchat/realtime/experimental/function_observer.py +2 -3
  37. autogen/agentchat/realtime/experimental/realtime_agent.py +2 -3
  38. autogen/agentchat/realtime/experimental/realtime_swarm.py +21 -10
  39. autogen/agentchat/user_proxy_agent.py +55 -53
  40. autogen/agents/experimental/document_agent/document_agent.py +1 -10
  41. autogen/agents/experimental/document_agent/parser_utils.py +5 -1
  42. autogen/browser_utils.py +4 -4
  43. autogen/cache/abstract_cache_base.py +2 -6
  44. autogen/cache/disk_cache.py +1 -6
  45. autogen/cache/in_memory_cache.py +2 -6
  46. autogen/cache/redis_cache.py +1 -5
  47. autogen/coding/__init__.py +10 -2
  48. autogen/coding/base.py +2 -1
  49. autogen/coding/docker_commandline_code_executor.py +1 -6
  50. autogen/coding/factory.py +9 -0
  51. autogen/coding/jupyter/docker_jupyter_server.py +1 -7
  52. autogen/coding/jupyter/jupyter_client.py +2 -9
  53. autogen/coding/jupyter/jupyter_code_executor.py +2 -7
  54. autogen/coding/jupyter/local_jupyter_server.py +2 -6
  55. autogen/coding/local_commandline_code_executor.py +0 -65
  56. autogen/coding/yepcode_code_executor.py +197 -0
  57. autogen/environments/docker_python_environment.py +3 -3
  58. autogen/environments/system_python_environment.py +5 -5
  59. autogen/environments/venv_python_environment.py +5 -5
  60. autogen/events/agent_events.py +1 -1
  61. autogen/events/client_events.py +1 -1
  62. autogen/fast_depends/utils.py +10 -0
  63. autogen/graph_utils.py +5 -7
  64. autogen/import_utils.py +3 -1
  65. autogen/interop/pydantic_ai/pydantic_ai.py +8 -5
  66. autogen/io/processors/console_event_processor.py +8 -3
  67. autogen/llm_config/config.py +168 -91
  68. autogen/llm_config/entry.py +38 -26
  69. autogen/llm_config/types.py +35 -0
  70. autogen/llm_config/utils.py +223 -0
  71. autogen/mcp/mcp_proxy/operation_grouping.py +48 -39
  72. autogen/messages/agent_messages.py +1 -1
  73. autogen/messages/client_messages.py +1 -1
  74. autogen/oai/__init__.py +8 -1
  75. autogen/oai/client.py +10 -3
  76. autogen/oai/client_utils.py +1 -1
  77. autogen/oai/cohere.py +4 -4
  78. autogen/oai/gemini.py +4 -6
  79. autogen/oai/gemini_types.py +1 -0
  80. autogen/oai/openai_utils.py +44 -115
  81. autogen/tools/dependency_injection.py +4 -8
  82. autogen/tools/experimental/reliable/reliable.py +3 -2
  83. autogen/tools/experimental/web_search_preview/web_search_preview.py +1 -1
  84. autogen/tools/function_utils.py +2 -1
  85. autogen/version.py +1 -1
  86. {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/WHEEL +0 -0
  87. {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/licenses/LICENSE +0 -0
  88. {ag2-0.9.9.dist-info → ag2-0.9.10.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,119 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+ from uuid import UUID
10
+
11
+ from termcolor import colored
12
+
13
+ from ....events.base_event import BaseEvent, wrap_event
14
+
15
+
16
+ @wrap_event
17
+ class SafeguardEvent(BaseEvent):
18
+ """Event for safeguard actions"""
19
+
20
+ event_type: str # e.g., "load", "check", "violation", "action"
21
+ message: str
22
+ source_agent: str | None = None
23
+ target_agent: str | None = None
24
+ guardrail_type: str | None = None
25
+ action: str | None = None
26
+ content_preview: str | None = None
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ uuid: UUID | None = None,
32
+ event_type: str,
33
+ message: str,
34
+ source_agent: str | None = None,
35
+ target_agent: str | None = None,
36
+ guardrail_type: str | None = None,
37
+ action: str | None = None,
38
+ content_preview: str | None = None,
39
+ ):
40
+ super().__init__(
41
+ uuid=uuid,
42
+ event_type=event_type,
43
+ message=message,
44
+ source_agent=source_agent,
45
+ target_agent=target_agent,
46
+ guardrail_type=guardrail_type,
47
+ action=action,
48
+ content_preview=content_preview,
49
+ )
50
+
51
+ def print(self, f: Callable[..., Any] | None = None) -> None:
52
+ f = f or print
53
+
54
+ # Choose color based on event type
55
+ color = "green"
56
+ if self.event_type == "load":
57
+ color = "green"
58
+ elif self.event_type == "check":
59
+ color = "cyan"
60
+ elif self.event_type == "violation":
61
+ color = "red"
62
+ elif self.event_type == "action":
63
+ color = "yellow"
64
+
65
+ # Choose emoji based on event type
66
+ emoji = ""
67
+ if self.event_type == "load":
68
+ emoji = "✅"
69
+ elif self.event_type == "check":
70
+ emoji = "🔍"
71
+ elif self.event_type == "violation":
72
+ emoji = "🛡️"
73
+ elif self.event_type == "action":
74
+ if self.action == "block":
75
+ emoji = "🚨"
76
+ elif self.action == "mask":
77
+ emoji = "🎭"
78
+ elif self.action == "warning":
79
+ emoji = "⚠️"
80
+ else:
81
+ emoji = "⚙️"
82
+
83
+ # Create header based on event type (skip for load events)
84
+ if self.event_type == "check":
85
+ header = f"***** Safeguard Check: {self.message} *****"
86
+ f(colored(header, color), flush=True)
87
+ elif self.event_type == "violation":
88
+ header = "***** Safeguard Violation: DETECTED *****"
89
+ f(colored(header, color), flush=True)
90
+ elif self.event_type == "action":
91
+ header = f"***** Safeguard Enforcement Action: {self.action.upper() if self.action else 'APPLIED'} *****"
92
+ f(colored(header, color), flush=True)
93
+
94
+ # Format the output
95
+ output_parts = [f"{emoji} {self.message}" if emoji else self.message]
96
+
97
+ if self.source_agent and self.target_agent:
98
+ output_parts.append(f" • From: {self.source_agent}")
99
+ output_parts.append(f" • To: {self.target_agent}")
100
+ elif self.source_agent:
101
+ output_parts.append(f" • Agent: {self.source_agent}")
102
+
103
+ if self.guardrail_type:
104
+ output_parts.append(f" • Guardrail: {self.guardrail_type}")
105
+
106
+ if self.action:
107
+ output_parts.append(f" • Action: {self.action}")
108
+
109
+ if self.content_preview:
110
+ # Replace actual newlines with \n for display
111
+ content_display = self.content_preview.replace("\n", "\\n").replace("\r", "\\r")
112
+ output_parts.append(f" • Content: {content_display}")
113
+
114
+ f(colored("\n".join(output_parts), color), flush=True)
115
+
116
+ # Print footer with matching length (skip for load events)
117
+ if self.event_type in ["check", "violation", "action"]:
118
+ footer = "*" * len(header)
119
+ f(colored(footer, color), flush=True)
@@ -0,0 +1,435 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from typing import Any
9
+
10
+
11
+ class SafeguardValidator:
12
+ """Validator for safeguard policy format and content."""
13
+
14
+ def __init__(self, policy: dict[str, Any]):
15
+ """Initialize validator with policy.
16
+
17
+ Args:
18
+ policy: The safeguard policy to validate
19
+ """
20
+ self.policy = policy
21
+
22
+ def validate_policy_structure(self) -> None:
23
+ """Validate policy format and syntax only."""
24
+ if not isinstance(self.policy, dict):
25
+ raise ValueError("Policy must be a dictionary")
26
+
27
+ # Validate inter-agent safeguards
28
+ if "inter_agent_safeguards" in self.policy:
29
+ self._validate_inter_agent_safeguards()
30
+
31
+ # Validate environment safeguards
32
+ if "agent_environment_safeguards" in self.policy:
33
+ self._validate_environment_safeguards()
34
+
35
+ def validate_policy_complete(self, agent_names: list[str], agent_tool_mapping: dict[str, list[str]]) -> None:
36
+ """Validate agent and tool names (assumes policy structure already validated).
37
+
38
+ Args:
39
+ agent_names: List of available agent names for validation
40
+ agent_tool_mapping: Mapping of agent names to their tool names
41
+ """
42
+ # Validate agent names
43
+ self.validate_agent_names(agent_names)
44
+
45
+ # Validate tool names if any tools exist
46
+ if any(tools for tools in agent_tool_mapping.values()):
47
+ self.validate_tool_names(agent_tool_mapping, agent_names)
48
+
49
+ def _validate_inter_agent_safeguards(self) -> None:
50
+ """Validate inter-agent safeguards section."""
51
+ inter_agent = self.policy["inter_agent_safeguards"]
52
+ if not isinstance(inter_agent, dict):
53
+ raise ValueError("inter_agent_safeguards must be a dictionary")
54
+
55
+ # Validate agent_transitions
56
+ if "agent_transitions" in inter_agent:
57
+ if not isinstance(inter_agent["agent_transitions"], list):
58
+ raise ValueError("agent_transitions must be a list")
59
+
60
+ for i, rule in enumerate(inter_agent["agent_transitions"]):
61
+ if not isinstance(rule, dict):
62
+ raise ValueError(f"agent_transitions[{i}] must be a dictionary")
63
+
64
+ # Required fields
65
+ required_fields = ["message_source", "message_destination"]
66
+ for field in required_fields:
67
+ if field not in rule:
68
+ raise ValueError(f"agent_transitions[{i}] missing required field: {field}")
69
+
70
+ # Check method validation - no default, must be explicit
71
+ if "check_method" not in rule:
72
+ raise ValueError(f"agent_transitions[{i}] missing required field: check_method")
73
+ check_method = rule["check_method"]
74
+ if check_method not in ["llm", "regex"]:
75
+ raise ValueError(
76
+ f"agent_transitions[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
77
+ )
78
+
79
+ # LLM-specific validation
80
+ if check_method == "llm":
81
+ if "custom_prompt" not in rule and "disallow_item" not in rule:
82
+ raise ValueError(
83
+ f"agent_transitions[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
84
+ )
85
+ if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
86
+ raise ValueError(f"agent_transitions[{i}] disallow_item must be a list")
87
+
88
+ # Regex-specific validation
89
+ if check_method == "regex":
90
+ if "pattern" not in rule:
91
+ raise ValueError(f"agent_transitions[{i}] with check_method 'regex' must have 'pattern'")
92
+ if not isinstance(rule["pattern"], str):
93
+ raise ValueError(f"agent_transitions[{i}] pattern must be a string")
94
+
95
+ # Test regex pattern validity
96
+ try:
97
+ re.compile(rule["pattern"])
98
+ except re.error as e:
99
+ raise ValueError(f"agent_transitions[{i}] invalid regex pattern '{rule['pattern']}': {e}")
100
+
101
+ # Validate action - no default, must be explicit
102
+ if "violation_response" not in rule and "action" not in rule:
103
+ raise ValueError(f"agent_transitions[{i}] missing required field: violation_response or action")
104
+ action = rule.get("violation_response", rule.get("action"))
105
+ if action not in ["block", "mask", "warning"]:
106
+ raise ValueError(
107
+ f"agent_transitions[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
108
+ )
109
+
110
+ # Validate groupchat_message_check
111
+ if "groupchat_message_check" in inter_agent:
112
+ rule = inter_agent["groupchat_message_check"]
113
+ if not isinstance(rule, dict):
114
+ raise ValueError("groupchat_message_check must be a dictionary")
115
+ if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
116
+ raise ValueError("groupchat_message_check disallow_item must be a list")
117
+
118
+ def _validate_environment_safeguards(self) -> None:
119
+ """Validate environment safeguards section."""
120
+ env_rules = self.policy["agent_environment_safeguards"]
121
+ if not isinstance(env_rules, dict):
122
+ raise ValueError("agent_environment_safeguards must be a dictionary")
123
+
124
+ # Validate tool_interaction rules
125
+ if "tool_interaction" in env_rules:
126
+ if not isinstance(env_rules["tool_interaction"], list):
127
+ raise ValueError("tool_interaction must be a list")
128
+
129
+ for i, rule in enumerate(env_rules["tool_interaction"]):
130
+ if not isinstance(rule, dict):
131
+ raise ValueError(f"tool_interaction[{i}] must be a dictionary")
132
+
133
+ # Check method validation - no default, must be explicit
134
+ if "check_method" not in rule:
135
+ raise ValueError(f"tool_interaction[{i}] missing required field: check_method")
136
+ check_method = rule["check_method"]
137
+ if check_method not in ["llm", "regex"]:
138
+ raise ValueError(
139
+ f"tool_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
140
+ )
141
+
142
+ # Validate action - no default, must be explicit
143
+ if "violation_response" not in rule and "action" not in rule:
144
+ raise ValueError(f"tool_interaction[{i}] missing required field: violation_response or action")
145
+ action = rule.get("violation_response", rule.get("action"))
146
+ if action not in ["block", "mask", "warning"]:
147
+ raise ValueError(
148
+ f"tool_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
149
+ )
150
+
151
+ # All tool_interaction rules must have message_source and message_destination
152
+ if "message_source" not in rule or "message_destination" not in rule:
153
+ raise ValueError(f"tool_interaction[{i}] must have 'message_source' and 'message_destination'")
154
+
155
+ if check_method == "llm":
156
+ # LLM-based checking requires either custom_prompt or disallow_item
157
+ if "custom_prompt" not in rule and "disallow_item" not in rule:
158
+ raise ValueError(
159
+ f"tool_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
160
+ )
161
+ if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
162
+ raise ValueError(f"tool_interaction[{i}] disallow_item must be a list")
163
+
164
+ elif check_method == "regex":
165
+ # Regex-based checking requires pattern
166
+ if "pattern" not in rule:
167
+ raise ValueError(f"tool_interaction[{i}] with check_method 'regex' must have 'pattern'")
168
+ if not isinstance(rule["pattern"], str):
169
+ raise ValueError(f"tool_interaction[{i}] pattern must be a string")
170
+ # Test regex pattern validity
171
+ try:
172
+ re.compile(rule["pattern"])
173
+ except re.error as e:
174
+ raise ValueError(f"tool_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
175
+
176
+ # Validate llm_interaction rules
177
+ if "llm_interaction" in env_rules:
178
+ if not isinstance(env_rules["llm_interaction"], list):
179
+ raise ValueError("llm_interaction must be a list")
180
+
181
+ for i, rule in enumerate(env_rules["llm_interaction"]):
182
+ if not isinstance(rule, dict):
183
+ raise ValueError(f"llm_interaction[{i}] must be a dictionary")
184
+
185
+ # Check method validation - no default, must be explicit
186
+ if "check_method" not in rule:
187
+ raise ValueError(f"llm_interaction[{i}] missing required field: check_method")
188
+ check_method = rule["check_method"]
189
+ if check_method not in ["llm", "regex"]:
190
+ raise ValueError(
191
+ f"llm_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
192
+ )
193
+
194
+ # Validate action - no default, must be explicit
195
+ if "action" not in rule:
196
+ raise ValueError(f"llm_interaction[{i}] missing required field: action")
197
+ action = rule["action"]
198
+ if action not in ["block", "mask", "warning"]:
199
+ raise ValueError(
200
+ f"llm_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
201
+ )
202
+
203
+ # All llm_interaction rules must have message_source and message_destination
204
+ if "message_source" not in rule or "message_destination" not in rule:
205
+ raise ValueError(f"llm_interaction[{i}] must have 'message_source' and 'message_destination'")
206
+
207
+ if check_method == "llm":
208
+ # LLM-based checking requires either custom_prompt or disallow_item
209
+ if "custom_prompt" not in rule and "disallow_item" not in rule:
210
+ raise ValueError(
211
+ f"llm_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
212
+ )
213
+ if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
214
+ raise ValueError(f"llm_interaction[{i}] disallow_item must be a list")
215
+
216
+ elif check_method == "regex":
217
+ # Regex-based checking requires pattern
218
+ if "pattern" not in rule:
219
+ raise ValueError(f"llm_interaction[{i}] with check_method 'regex' must have 'pattern'")
220
+ if not isinstance(rule["pattern"], str):
221
+ raise ValueError(f"llm_interaction[{i}] pattern must be a string")
222
+ # Test regex pattern validity
223
+ try:
224
+ re.compile(rule["pattern"])
225
+ except re.error as e:
226
+ raise ValueError(f"llm_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
227
+
228
+ # Validate user_interaction rules
229
+ if "user_interaction" in env_rules:
230
+ if not isinstance(env_rules["user_interaction"], list):
231
+ raise ValueError("user_interaction must be a list")
232
+
233
+ for i, rule in enumerate(env_rules["user_interaction"]):
234
+ if not isinstance(rule, dict):
235
+ raise ValueError(f"user_interaction[{i}] must be a dictionary")
236
+
237
+ # Check method validation - no default, must be explicit
238
+ if "check_method" not in rule:
239
+ raise ValueError(f"user_interaction[{i}] missing required field: check_method")
240
+ check_method = rule["check_method"]
241
+ if check_method not in ["llm", "regex"]:
242
+ raise ValueError(
243
+ f"user_interaction[{i}] invalid check_method: {check_method}. Must be 'llm' or 'regex'"
244
+ )
245
+
246
+ # Validate action - no default, must be explicit
247
+ if "action" not in rule:
248
+ raise ValueError(f"user_interaction[{i}] missing required field: action")
249
+ action = rule["action"]
250
+ if action not in ["block", "mask", "warning"]:
251
+ raise ValueError(
252
+ f"user_interaction[{i}] invalid action: {action}. Must be 'block', 'mask', or 'warning'"
253
+ )
254
+
255
+ # All user_interaction rules must have message_source and message_destination
256
+ if "message_source" not in rule or "message_destination" not in rule:
257
+ raise ValueError(f"user_interaction[{i}] must have 'message_source' and 'message_destination'")
258
+
259
+ if check_method == "llm":
260
+ # LLM-based checking requires either custom_prompt or disallow_item
261
+ if "custom_prompt" not in rule and "disallow_item" not in rule:
262
+ raise ValueError(
263
+ f"user_interaction[{i}] with check_method 'llm' must have either 'custom_prompt' or 'disallow_item'"
264
+ )
265
+ if "disallow_item" in rule and not isinstance(rule["disallow_item"], list):
266
+ raise ValueError(f"user_interaction[{i}] disallow_item must be a list")
267
+
268
+ elif check_method == "regex":
269
+ # Regex-based checking requires pattern
270
+ if "pattern" not in rule:
271
+ raise ValueError(f"user_interaction[{i}] with check_method 'regex' must have 'pattern'")
272
+ if not isinstance(rule["pattern"], str):
273
+ raise ValueError(f"user_interaction[{i}] pattern must be a string")
274
+ # Test regex pattern validity
275
+ try:
276
+ re.compile(rule["pattern"])
277
+ except re.error as e:
278
+ raise ValueError(f"user_interaction[{i}] invalid regex pattern '{rule['pattern']}': {e}")
279
+
280
+ def validate_agent_names(self, agent_names: list[str]) -> None:
281
+ """Validate that agent names referenced in policy actually exist."""
282
+ available_agents = set(agent_names)
283
+
284
+ # Check inter-agent safeguards
285
+ if "inter_agent_safeguards" in self.policy:
286
+ inter_agent = self.policy["inter_agent_safeguards"]
287
+
288
+ # Check agent_transitions
289
+ for i, rule in enumerate(inter_agent.get("agent_transitions", [])):
290
+ src_agent = rule.get("message_source")
291
+ dst_agent = rule.get("message_destination")
292
+
293
+ # Skip wildcard patterns
294
+ if src_agent != "*" and src_agent not in available_agents:
295
+ raise ValueError(
296
+ f"agent_transitions[{i}] references unknown source agent: '{src_agent}'. Available agents: {sorted(available_agents)}"
297
+ )
298
+
299
+ if dst_agent != "*" and dst_agent not in available_agents:
300
+ raise ValueError(
301
+ f"agent_transitions[{i}] references unknown destination agent: '{dst_agent}'. Available agents: {sorted(available_agents)}"
302
+ )
303
+
304
+ # Check environment safeguards
305
+ if "agent_environment_safeguards" in self.policy:
306
+ env_rules = self.policy["agent_environment_safeguards"]
307
+
308
+ # Check tool_interaction rules - only support message_source/message_destination format
309
+ for i, rule in enumerate(env_rules.get("tool_interaction", [])):
310
+ # Only validate message_source/message_destination format
311
+ if "message_source" in rule and "message_destination" in rule:
312
+ # Skip detailed validation since we can't distinguish agent vs tool names
313
+ pass
314
+ elif "pattern" in rule and "message_source" not in rule:
315
+ # Simple pattern rules are allowed
316
+ pass
317
+ else:
318
+ raise ValueError(
319
+ f"tool_interaction[{i}] must use either (message_source, message_destination) or pattern-only format"
320
+ )
321
+
322
+ # Check llm_interaction rules
323
+ for i, rule in enumerate(env_rules.get("llm_interaction", [])):
324
+ # New format
325
+ if "message_source" in rule and "message_destination" in rule:
326
+ src = rule["message_source"]
327
+ dst = rule["message_destination"]
328
+
329
+ # Check agent references (LLM interactions have agent <-> llm)
330
+ if src != "llm" and src.lower() != "llm" and src not in available_agents:
331
+ raise ValueError(
332
+ f"llm_interaction[{i}] references unknown agent: '{src}'. Available agents: {sorted(available_agents)}"
333
+ )
334
+ if dst != "llm" and dst.lower() != "llm" and dst not in available_agents:
335
+ raise ValueError(
336
+ f"llm_interaction[{i}] references unknown agent: '{dst}'. Available agents: {sorted(available_agents)}"
337
+ )
338
+
339
+ elif "agent_name" in rule:
340
+ agent_name = rule["agent_name"]
341
+ if agent_name not in available_agents:
342
+ raise ValueError(
343
+ f"llm_interaction[{i}] references unknown agent: '{agent_name}'. Available agents: {sorted(available_agents)}"
344
+ )
345
+
346
+ # Check user_interaction rules
347
+ for i, rule in enumerate(env_rules.get("user_interaction", [])):
348
+ agent_name = rule.get("agent")
349
+ if agent_name and agent_name not in available_agents:
350
+ raise ValueError(
351
+ f"user_interaction[{i}] references unknown agent: '{agent_name}'. Available agents: {sorted(available_agents)}"
352
+ )
353
+
354
+ def validate_tool_names(self, agent_tool_mapping: dict[str, list[str]], agent_names: list[str]) -> None:
355
+ """Validate that tool names referenced in policy actually exist and belong to the correct agents.
356
+
357
+ Args:
358
+ agent_tool_mapping: Dict mapping agent names to their tool names
359
+ agent_names: List of available agent names
360
+ """
361
+ available_agents = set(agent_names)
362
+ # Get all available tools across all agents
363
+ all_available_tools = set()
364
+ for tools in agent_tool_mapping.values():
365
+ all_available_tools.update(tools)
366
+
367
+ # Check environment safeguards for tool references
368
+ if "agent_environment_safeguards" in self.policy:
369
+ env_rules = self.policy["agent_environment_safeguards"]
370
+
371
+ # Check tool_interaction rules
372
+ for i, rule in enumerate(env_rules.get("tool_interaction", [])):
373
+ # Check message_source/message_destination format
374
+ if "message_source" in rule and "message_destination" in rule:
375
+ src = rule["message_source"]
376
+ dst = rule["message_destination"]
377
+
378
+ # Validate agent-tool relationships
379
+ self._validate_agent_tool_relationship(
380
+ i, "message_source", src, dst, available_agents, agent_tool_mapping, all_available_tools
381
+ )
382
+
383
+ def _validate_agent_tool_relationship(
384
+ self,
385
+ rule_index: int,
386
+ src_field: str,
387
+ src: str,
388
+ dst: str,
389
+ available_agents: set[str],
390
+ agent_tool_mapping: dict[str, list[str]],
391
+ all_available_tools: set[str],
392
+ ) -> None:
393
+ """Validate that agent-tool relationships in policy are correct."""
394
+ # Skip wildcards and special cases
395
+ if src == "*" or dst == "*" or src.lower() == "user" or dst.lower() == "user":
396
+ return
397
+
398
+ # Case 1: Agent -> Tool (agent uses tool)
399
+ if src in available_agents and dst not in available_agents:
400
+ # dst should be a tool that belongs to src agent
401
+ if dst not in all_available_tools:
402
+ raise ValueError(
403
+ f"tool_interaction[{rule_index}] references unknown tool: '{dst}'. Available tools: {sorted(all_available_tools)}"
404
+ )
405
+ if dst not in agent_tool_mapping.get(src, []):
406
+ agent_tools = agent_tool_mapping.get(src, [])
407
+ raise ValueError(
408
+ f"tool_interaction[{rule_index}] agent '{src}' does not have access to tool '{dst}'. "
409
+ f"Agent's tools: {sorted(agent_tools)}"
410
+ )
411
+
412
+ # Case 2: Tool -> Agent (tool output to agent)
413
+ elif src not in available_agents and dst in available_agents:
414
+ # src should be a tool that belongs to dst agent
415
+ if src not in all_available_tools:
416
+ raise ValueError(
417
+ f"tool_interaction[{rule_index}] references unknown tool: '{src}'. Available tools: {sorted(all_available_tools)}"
418
+ )
419
+ if src not in agent_tool_mapping.get(dst, []):
420
+ agent_tools = agent_tool_mapping.get(dst, [])
421
+ raise ValueError(
422
+ f"tool_interaction[{rule_index}] agent '{dst}' does not have access to tool '{src}'. "
423
+ f"Agent's tools: {sorted(agent_tools)}"
424
+ )
425
+
426
+ # Case 3: Tool -> Tool (unusual, but validate both exist)
427
+ elif src not in available_agents and dst not in available_agents:
428
+ if src not in all_available_tools:
429
+ raise ValueError(
430
+ f"tool_interaction[{rule_index}] references unknown tool: '{src}'. Available tools: {sorted(all_available_tools)}"
431
+ )
432
+ if dst not in all_available_tools:
433
+ raise ValueError(
434
+ f"tool_interaction[{rule_index}] references unknown tool: '{dst}'. Available tools: {sorted(all_available_tools)}"
435
+ )