diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 0d2f2044..de913824 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -501,7 +501,7 @@ import agent_control agent_control.init( agent_name="my-agent", # Required: human-readable name - agent_id="my-agent-v1", # Required: unique identifier + agent_id="550e8400-e29b-41d4-a716-446655440000", # Required: UUID server_url="http://localhost:8000", # Optional: defaults to env var steps=[ # Optional: register available steps { diff --git a/examples/README.md b/examples/README.md index ff53a8a5..88febbc6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -92,7 +92,10 @@ import agent_control from agent_control import control, ControlViolationError # Initialize agent (connects to server, loads policy) -agent_control.init(agent_name="my-bot", agent_id="bot-123") +agent_control.init( + agent_name="my-bot", + agent_id="550e8400-e29b-41d4-a716-446655440000", +) # Apply the agent's assigned policy @control() diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 268b5953..8a1e7cfe 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -44,7 +44,7 @@ # Configuration AGENT_NAME = "demo-chatbot" -AGENT_ID = "demo-chatbot-v1" +AGENT_ID = "672e50df-af4c-429f-965a-3d7f8262302f" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -258,4 +258,3 @@ async def run_demo(): if __name__ == "__main__": asyncio.run(run_demo()) - diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index 30c5be35..cf4b6c73 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -34,7 +34,7 @@ # Configuration AGENT_NAME = "demo-chatbot" -AGENT_ID = "demo-chatbot-v1" +AGENT_ID = "672e50df-af4c-429f-965a-3d7f8262302f" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -44,9 +44,8 @@ async def create_agent(client: AgentControlClient) -> str: print("STEP 1: Creating Agent") print("=" * 60) - # Generate a deterministic UUID from the agent ID - import uuid - agent_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID) + # Use the provided UUID for the agent + agent_uuid = UUID(AGENT_ID) try: response = await client.http_client.post( @@ -440,9 +439,8 @@ async def main(): print(" cd server && make run") return - # Generate agent UUID - import uuid - agent_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + # Use the provided UUID for verification + agent_uuid = AGENT_ID # If verify-only mode, just run verification if args.verify_only: diff --git a/examples/agent_control_demo/update_controls.py b/examples/agent_control_demo/update_controls.py index 8430ed40..6c7ab3f9 100644 --- a/examples/agent_control_demo/update_controls.py +++ b/examples/agent_control_demo/update_controls.py @@ -19,15 +19,13 @@ import asyncio import os import sys -import uuid - # Add the SDK to path for development sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../sdks/python/src")) from agent_control import AgentControlClient # Configuration -AGENT_ID = "demo-chatbot-v1" +AGENT_ID = "672e50df-af4c-429f-965a-3d7f8262302f" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -156,8 +154,8 @@ async def main(): parser.add_argument("--status", action="store_true", help="Show current status") args = parser.parse_args() - # Generate agent UUID - agent_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + # Use the provided UUID + agent_uuid = AGENT_ID print("\n" + "=" * 60) print("AGENT CONTROL DEMO: Update Controls") diff --git a/examples/crewai/content_agent_protection.py b/examples/crewai/content_agent_protection.py index f1d3f50f..118be45a 100644 --- a/examples/crewai/content_agent_protection.py +++ b/examples/crewai/content_agent_protection.py @@ -45,7 +45,7 @@ # It's a dependency of agent_control so should be available # --- Configuration --- -AGENT_ID = "support-crew-demo" +AGENT_ID = "989d84f0-9afe-4fb2-9e9e-e9d076271e29" AGENT_NAME = "Customer Support Crew" AGENT_DESCRIPTION = "Customer support crew with PII protection and access controls" diff --git a/examples/crewai/setup_content_controls.py b/examples/crewai/setup_content_controls.py index 612b15e1..94674e4d 100644 --- a/examples/crewai/setup_content_controls.py +++ b/examples/crewai/setup_content_controls.py @@ -10,11 +10,11 @@ import asyncio import os -import uuid +from uuid import UUID from agent_control import Agent, AgentControlClient, agents, controls, policies -AGENT_ID = "support-crew-demo" +AGENT_ID = "989d84f0-9afe-4fb2-9e9e-e9d076271e29" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -22,7 +22,7 @@ async def setup_content_controls(): """Create PII protection and unauthorized access controls, policy, and assign to agent.""" async with AgentControlClient(base_url=SERVER_URL) as client: # 1. Register Agent - agent_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID) + agent_uuid = UUID(AGENT_ID) agent = Agent( agent_id=agent_uuid, diff --git a/examples/customer_support_agent/README.md b/examples/customer_support_agent/README.md index c99e5f5d..98e88360 100644 --- a/examples/customer_support_agent/README.md +++ b/examples/customer_support_agent/README.md @@ -161,7 +161,7 @@ import agent_control agent_control.init( agent_name="Customer Support Agent", - agent_id="customer-support-agent", + agent_id="646d5dea-c2e6-4453-b446-7035482b38e4", agent_description="AI-powered customer support assistant", ) ``` diff --git a/examples/customer_support_agent/demo.sh b/examples/customer_support_agent/demo.sh index cada15cc..859c338a 100755 --- a/examples/customer_support_agent/demo.sh +++ b/examples/customer_support_agent/demo.sh @@ -222,9 +222,9 @@ setup_demo_controls() { python setup_demo_controls.py } -# Generate UUID5 from agent ID (matches Python's uuid.uuid5(uuid.NAMESPACE_DNS, agent_id)) +# Agent UUID used by the demo (must match support_agent.py) get_agent_uuid() { - python3 -c "import uuid; print(uuid.uuid5(uuid.NAMESPACE_DNS, 'customer-support-agent'))" + echo "646d5dea-c2e6-4453-b446-7035482b38e4" } show_observability_stats() { diff --git a/examples/customer_support_agent/run_demo.py b/examples/customer_support_agent/run_demo.py index 099eeb66..84eac08a 100644 --- a/examples/customer_support_agent/run_demo.py +++ b/examples/customer_support_agent/run_demo.py @@ -54,16 +54,13 @@ # Add parent directory to path for imports sys.path.insert(0, str(__file__).rsplit("/", 2)[0]) -# Agent ID used in support_agent.py -AGENT_ID = "customer-support-agent" +# Agent UUID used in support_agent.py +AGENT_ID = "646d5dea-c2e6-4453-b446-7035482b38e4" async def reset_agent(): """Reset the agent by removing its policy (which disconnects all controls).""" - import uuid - - # Generate the same UUID5 that the SDK generates - agent_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + agent_uuid = AGENT_ID server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") logger.info(f"Resetting agent '{AGENT_ID}' (UUID: {agent_uuid})") diff --git a/examples/customer_support_agent/setup_demo_controls.py b/examples/customer_support_agent/setup_demo_controls.py index 3c4b686b..904f38ce 100644 --- a/examples/customer_support_agent/setup_demo_controls.py +++ b/examples/customer_support_agent/setup_demo_controls.py @@ -13,12 +13,10 @@ import asyncio import os -import uuid - from agent_control import Agent, AgentControlClient, agents, controls, policies # Same agent ID as in support_agent.py -AGENT_ID = "customer-support-agent" +AGENT_ID = "646d5dea-c2e6-4453-b446-7035482b38e4" AGENT_NAME = "Customer Support Agent" AGENT_DESCRIPTION = "AI-powered customer support assistant" @@ -228,8 +226,8 @@ async def setup_demo(quiet: bool = False): """Set up the demo agent with controls.""" - # Generate the same UUID5 that the SDK generates - agent_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + # Use the provided UUID (must match support_agent.py) + agent_uuid = AGENT_ID async with AgentControlClient(base_url=SERVER_URL, timeout=30.0) as client: # Check server health diff --git a/examples/customer_support_agent/support_agent.py b/examples/customer_support_agent/support_agent.py index 80267e71..74328a11 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -30,7 +30,7 @@ agent_control.init( agent_name="Customer Support Agent", - agent_id="customer-support-agent", + agent_id="646d5dea-c2e6-4453-b446-7035482b38e4", agent_description="AI-powered customer support assistant that helps with inquiries, " "searches knowledge bases, and creates support tickets.", agent_version="1.0.0", diff --git a/examples/langchain/setup_sql_controls.py b/examples/langchain/setup_sql_controls.py index f288193a..59bafda2 100644 --- a/examples/langchain/setup_sql_controls.py +++ b/examples/langchain/setup_sql_controls.py @@ -13,13 +13,13 @@ import asyncio import os import pathlib -import uuid +from uuid import UUID import requests from agent_control import Agent, AgentControlClient, agents, controls, policies -AGENT_ID = "sql-agent-demo" +AGENT_ID = "edf66504-0db5-4ee8-9e09-3ef37bbb8faa" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -52,7 +52,7 @@ async def setup_sql_controls(): """Create SQL control, policy, and assign to agent.""" async with AgentControlClient(base_url=SERVER_URL) as client: # 1. Register Agent - agent_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID) + agent_uuid = UUID(AGENT_ID) agent = Agent( agent_id=agent_uuid, diff --git a/examples/langchain/sql_agent_protection.py b/examples/langchain/sql_agent_protection.py index c90301e0..3e6dfb08 100644 --- a/examples/langchain/sql_agent_protection.py +++ b/examples/langchain/sql_agent_protection.py @@ -50,7 +50,7 @@ from langgraph.prebuilt import ToolNode # --- Configuration --- -AGENT_ID = "sql-agent-demo" +AGENT_ID = "edf66504-0db5-4ee8-9e09-3ef37bbb8faa" AGENT_NAME = "SQL Demo Agent" AGENT_DESCRIPTION = "SQL agent with server-side controls" USE_LOCAL_CONTROLS = os.getenv("AGENT_CONTROL_LOCAL_EVAL", "false").lower() == "true" diff --git a/models/README.md b/models/README.md index e813b79b..df73c129 100644 --- a/models/README.md +++ b/models/README.md @@ -50,7 +50,7 @@ from agent_control_models import Agent, Step # Create an agent agent = Agent( agent_name="Customer Support Bot", - agent_id="support-bot-v1", + agent_id="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries", agent_version="1.0.0" ) @@ -129,7 +129,7 @@ Agent metadata and configuration. **Fields:** - `agent_name` (str): Human-readable agent name -- `agent_id` (str): Unique identifier +- `agent_id` (UUID): Unique identifier - `agent_description` (Optional[str]): Agent description - `agent_version` (Optional[str]): Agent version - `tools` (Optional[List[str]]): List of available tools @@ -198,7 +198,7 @@ from agent_control_models import Agent # Create with validation agent = Agent( agent_name="My Agent", - agent_id="my-agent-v1" + agent_id="550e8400-e29b-41d4-a716-446655440000" ) # Serialize to dict @@ -243,7 +243,7 @@ from agent_control_models import Agent # Add custom metadata agent = Agent( agent_name="Support Bot", - agent_id="support-v1", + agent_id="550e8400-e29b-41d4-a716-446655440000", metadata={ "team": "customer-success", "environment": "production", @@ -291,4 +291,3 @@ uv run pytest 4. **Version carefully**: Model changes affect both server and SDK 5. **Document fields**: Use Field's `description` parameter 6. **Use Optional appropriately**: Mark optional fields clearly - diff --git a/sdks/python/ARCHITECTURE.md b/sdks/python/ARCHITECTURE.md index 6af59b13..86625b10 100644 --- a/sdks/python/ARCHITECTURE.md +++ b/sdks/python/ARCHITECTURE.md @@ -66,7 +66,9 @@ async with agent_control.AgentControlClient() as client: ) # Get agent - agent_data = await agent_control.agents.get_agent(client, "agent-id") + agent_data = await agent_control.agents.get_agent( + client, "550e8400-e29b-41d4-a716-446655440000" + ) ``` ### `policies.py` - Policy Operations @@ -259,11 +261,13 @@ High-level convenience functions are provided in `__init__.py` for common operat ```python # Convenience: No need to manage client manually -agent_data = await agent_control.get_agent("agent-id") +agent_data = await agent_control.get_agent("550e8400-e29b-41d4-a716-446655440000") # Equivalent module-first approach: async with AgentControlClient() as client: - agent_data = await agent_control.agents.get_agent(client, "agent-id") + agent_data = await agent_control.agents.get_agent( + client, "550e8400-e29b-41d4-a716-446655440000" + ) ``` ### 4. API Stability diff --git a/sdks/python/README.md b/sdks/python/README.md index fb4e75d5..8607c60b 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -18,7 +18,7 @@ import agent_control # Initialize at the base of your agent agent_control.init( agent_name="My Customer Service Bot", - agent_id="csbot-prod-v1" + agent_id="550e8400-e29b-41d4-a716-446655440000" ) # Use the control decorator @@ -36,7 +36,7 @@ import agent_control agent_control.init( agent_name="Customer Service Bot", - agent_id="csbot-prod-v1", + agent_id="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries and support", agent_version="2.1.0", server_url="http://localhost:8000", @@ -53,7 +53,10 @@ agent_control.init( One line to set up your agent with full protection: ```python -agent_control.init(agent_name="...", agent_id="...") +agent_control.init( + agent_name="...", + agent_id="550e8400-e29b-41d4-a716-446655440000", +) ``` This automatically: @@ -87,7 +90,7 @@ async with AgentControlClient() as client: # Evaluate a step result = await agent_control.evaluation.check_evaluation( client, - agent_uuid="your-agent-uuid", + agent_uuid="550e8400-e29b-41d4-a716-446655440000", step={"type": "llm_inference", "input": "User input here"}, stage="pre" ) @@ -114,7 +117,7 @@ from agent_control import control, ControlViolationError # Initialize agent_control.init( agent_name="Customer Support Bot", - agent_id="support-bot-v1", + agent_id="550e8400-e29b-41d4-a716-446655440000", agent_version="1.0.0" ) @@ -155,7 +158,7 @@ asyncio.run(main()) ```python def init( agent_name: str, - agent_id: str, + agent_id: str | UUID, agent_description: Optional[str] = None, agent_version: Optional[str] = None, server_url: Optional[str] = None, @@ -168,7 +171,7 @@ Initialize Agent Control with your agent's information. **Parameters:** - `agent_name`: Human-readable name -- `agent_id`: Unique identifier (user-defined) +- `agent_id`: UUID string (or UUID instance) - `agent_description`: Optional description - `agent_version`: Optional version string - `server_url`: Optional server URL (defaults to `AGENT_CONTROL_URL` env var) @@ -273,7 +276,10 @@ Basic usage: import agent_control from agent_control import control, ControlViolationError -agent_control.init(agent_name="...", agent_id="...") +agent_control.init( + agent_name="...", + agent_id="550e8400-e29b-41d4-a716-446655440000", +) @control() async def handle(message: str): diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index fbda576a..9d2d342d 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -10,7 +10,7 @@ # Initialize at the base of your agent file agent_control.init( agent_name="my-customer-service-bot", - agent_id="csbot-prod-v1" + agent_id="550e8400-e29b-41d4-a716-446655440000" ) # Apply server-defined controls using the decorator @@ -40,6 +40,8 @@ async def process(input: str) -> str: from typing import TYPE_CHECKING, Any, Literal, TypeVar from uuid import UUID +import httpx + from . import agents, controls, evaluation, evaluators, policies # Import client and operations modules @@ -69,6 +71,7 @@ async def process(input: str) -> str: is_otel_available, with_trace, ) +from .validation import ensure_uuid # Module logger logger = get_logger(__name__) @@ -188,7 +191,7 @@ def __init__( def init( agent_name: str, - agent_id: str, + agent_id: str | UUID, agent_description: str | None = None, agent_version: str | None = None, server_url: str | None = None, @@ -212,9 +215,7 @@ def init( Args: agent_name: Human-readable name for your agent (e.g., "Customer Service Bot") - agent_id: Unique identifier for your agent. Can be: - - A UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000") - - Any string (e.g., "csbot-prod-v1") - will be converted to UUID + agent_id: Unique identifier for your agent (UUID string or UUID instance) agent_description: Optional description of what your agent does agent_version: Optional version string (e.g., "1.0.0") server_url: Optional server URL (defaults to AGENT_CONTROL_URL env var @@ -236,7 +237,7 @@ def init( agent_control.init( agent_name="Customer Service Bot", - agent_id="csbot-prod-v1", + agent_id="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries and support tickets", agent_version="2.1.0", steps=[ @@ -264,25 +265,19 @@ async def handle(message: str): if not agent_id: raise ValueError( "The 'agent_id' argument is required for initialization.\n" - "Please provide a unique string identifier for your agent, e.g.:\n" - ' agent_control.init(agent_name="my-agent", agent_id="my-agent-v1")' + "Please provide a valid UUID string for your agent, e.g.:\n" + ' agent_control.init(agent_name="my-agent", ' + 'agent_id="550e8400-e29b-41d4-a716-446655440000")' ) + # Validate agent_id is a UUID (string or UUID instance) + _agent_uuid = ensure_uuid(agent_id) + # Configure logging if provided (do this early before any logging happens) if log_config: configure_logging(log_config) # Create agent instance with metadata - # Convert agent_id to UUID (accept UUID string or generate from regular string) - try: - _agent_uuid = UUID(agent_id) - except ValueError: - # If not a valid UUID, generate UUID5 (namespace-based, deterministic) - # Using DNS namespace for consistency - import uuid - _agent_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, agent_id) - logger.info("Generated UUID5 %s from agent_id '%s'", _agent_uuid, agent_id) - _current_agent = Agent( agent_id=_agent_uuid, agent_name=agent_name, @@ -331,6 +326,9 @@ async def register() -> list[dict[str, Any]] | None: logger.debug("Registered %d step(s)", len(steps)) return controls + except httpx.HTTPStatusError: + # Surface API errors like UUID conflicts + raise except Exception as e: logger.error("Failed to register agent: %s", e, exc_info=True) return None @@ -370,6 +368,9 @@ def run_in_thread() -> None: server_controls = loop.run_until_complete(register()) loop.close() + except httpx.HTTPStatusError: + # Surface server-side errors (e.g., 409 UUID conflicts) + raise except Exception as e: logger.error("Could not connect to server: %s", e, exc_info=True) logger.info("Will use local controls if available") @@ -397,7 +398,7 @@ def run_in_thread() -> None: async def get_agent( - agent_id: str, + agent_id: str | UUID, server_url: str | None = None, api_key: str | None = None, ) -> dict[str, Any]: @@ -405,7 +406,7 @@ async def get_agent( Get agent details from the server by ID. Args: - agent_id: UUID or string identifier of the agent + agent_id: UUID string or UUID instance server_url: Optional server URL (defaults to AGENT_CONTROL_URL env var) api_key: Optional API key for authentication (defaults to AGENT_CONTROL_API_KEY env var) @@ -423,7 +424,9 @@ async def get_agent( # Fetch agent from server async def main(): - agent_data = await agent_control.get_agent("bot-123") + agent_data = await agent_control.get_agent( + "550e8400-e29b-41d4-a716-446655440000" + ) print(f"Agent: {agent_data['agent']['agent_name']}") print(f"Steps: {len(agent_data['steps'])}") @@ -431,7 +434,9 @@ async def main(): # Or using the client directly async with agent_control.AgentControlClient() as client: - agent_data = await agent_control.agents.get_agent(client, "bot-123") + agent_data = await agent_control.agents.get_agent( + client, "550e8400-e29b-41d4-a716-446655440000" + ) """ _final_server_url = server_url or os.getenv('AGENT_CONTROL_URL') or 'http://localhost:8000' @@ -447,7 +452,10 @@ def current_agent() -> Agent | None: Current Agent instance or None if not initialized Example: - agent_control.init(agent_name="My Bot", agent_id="bot-123") + agent_control.init( + agent_name="My Bot", + agent_id="550e8400-e29b-41d4-a716-446655440000", + ) agent = agent_control.current_agent() print(agent.agent_name) # "My Bot" """ diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index dec6b4a9..b7b3fd33 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -6,6 +6,7 @@ from agent_control_engine import ensure_evaluators_discovered from .client import AgentControlClient +from .validation import ensure_uuid_str # Import models if available try: @@ -74,14 +75,14 @@ async def register_agent( async def get_agent( client: AgentControlClient, - agent_id: str + agent_id: str | UUID ) -> dict[str, Any]: """ Get agent details by ID from the server. Args: client: AgentControlClient instance - agent_id: UUID or string identifier of the agent + agent_id: UUID string or UUID instance Returns: Dictionary containing: @@ -97,7 +98,8 @@ async def get_agent( print(f"Agent: {agent_data['agent']['agent_name']}") print(f"Steps: {len(agent_data['steps'])}") """ - response = await client.http_client.get(f"/api/v1/agents/{agent_id}") + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}") response.raise_for_status() return cast(dict[str, Any], response.json()) @@ -146,14 +148,14 @@ async def list_agents( async def get_agent_policy( client: AgentControlClient, - agent_id: str, + agent_id: str | UUID, ) -> dict[str, Any]: """ Get the policy assigned to an agent. Args: client: AgentControlClient instance - agent_id: UUID or string identifier of the agent + agent_id: UUID string or UUID instance Returns: Dictionary containing: @@ -167,21 +169,22 @@ async def get_agent_policy( policy = await get_agent_policy(client, agent_id) print(f"Policy ID: {policy['policy_id']}") """ - response = await client.http_client.get(f"/api/v1/agents/{agent_id}/policy") + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/policy") response.raise_for_status() return cast(dict[str, Any], response.json()) async def remove_agent_policy( client: AgentControlClient, - agent_id: str, + agent_id: str | UUID, ) -> dict[str, Any]: """ Remove the policy assignment from an agent. Args: client: AgentControlClient instance - agent_id: UUID or string identifier of the agent + agent_id: UUID string or UUID instance Returns: Dictionary containing success flag/details @@ -189,6 +192,7 @@ async def remove_agent_policy( Raises: httpx.HTTPError: If request fails or agent has no policy """ - response = await client.http_client.delete(f"/api/v1/agents/{agent_id}/policy") + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.delete(f"/api/v1/agents/{agent_id_str}/policy") response.raise_for_status() return cast(dict[str, Any], response.json()) diff --git a/sdks/python/src/agent_control/control_decorators.py b/sdks/python/src/agent_control/control_decorators.py index 247edc20..a2618054 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -11,7 +11,10 @@ Usage: import agent_control - agent_control.init(agent_name="my-agent", agent_id="agent-123") + agent_control.init( + agent_name="my-agent", + agent_id="550e8400-e29b-41d4-a716-446655440000", + ) # Apply the agent's assigned policy @agent_control.control() @@ -502,7 +505,10 @@ def control(policy: str | None = None) -> Callable[[F], F]: import agent_control # Initialize agent (connects to server, loads policy) - agent_control.init(agent_name="my-bot", agent_id="bot-123") + agent_control.init( + agent_name="my-bot", + agent_id="550e8400-e29b-41d4-a716-446655440000", + ) # Apply the agent's policy (all controls) @agent_control.apply_control() diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index 9c67be50..dab7ecaa 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -1,8 +1,10 @@ """Policy management operations for Agent Control SDK.""" from typing import Any, cast +from uuid import UUID from .client import AgentControlClient +from .validation import ensure_uuid_str async def create_policy( @@ -150,7 +152,7 @@ async def list_policy_controls( async def assign_policy_to_agent( client: AgentControlClient, - agent_id: str, + agent_id: str | UUID, policy_id: int ) -> dict[str, Any]: """ @@ -160,7 +162,7 @@ async def assign_policy_to_agent( Args: client: AgentControlClient instance - agent_id: UUID or string identifier of the agent + agent_id: UUID string or UUID instance policy_id: ID of the policy to assign Returns: @@ -170,9 +172,9 @@ async def assign_policy_to_agent( httpx.HTTPError: If request fails HTTPException 404: Agent or policy not found """ + agent_id_str = ensure_uuid_str(agent_id) response = await client.http_client.post( - f"/api/v1/agents/{agent_id}/policy/{policy_id}" + f"/api/v1/agents/{agent_id_str}/policy/{policy_id}" ) response.raise_for_status() return cast(dict[str, Any], response.json()) - diff --git a/sdks/python/src/agent_control/validation.py b/sdks/python/src/agent_control/validation.py new file mode 100644 index 00000000..aaf95ff3 --- /dev/null +++ b/sdks/python/src/agent_control/validation.py @@ -0,0 +1,20 @@ +"""Validation helpers for SDK inputs.""" + +from __future__ import annotations + +from uuid import UUID + + +def ensure_uuid(value: str | UUID, field_name: str = "agent_id") -> UUID: + """Return a UUID instance or raise ValueError for invalid UUID strings.""" + if isinstance(value, UUID): + return value + try: + return UUID(str(value)) + except (TypeError, ValueError, AttributeError) as exc: + raise ValueError(f"{field_name} must be a valid UUID string") from exc + + +def ensure_uuid_str(value: str | UUID, field_name: str = "agent_id") -> str: + """Return a UUID string or raise ValueError for invalid UUID strings.""" + return str(ensure_uuid(value, field_name=field_name)) diff --git a/sdks/python/tests/README.md b/sdks/python/tests/README.md index 7282d5a6..71e690d7 100644 --- a/sdks/python/tests/README.md +++ b/sdks/python/tests/README.md @@ -152,7 +152,7 @@ PASSED ### Function-Scoped Fixtures - `client`: Authenticated AgentProtectClient instance - `unique_name`: Unique name generator for test resources -- `test_agent_id`: Unique agent ID for testing +- `test_agent_id`: Unique agent UUID for testing - `test_agent`: Registered test agent - `test_policy`: Created test policy - `test_control`: Created test control diff --git a/sdks/python/tests/conftest.py b/sdks/python/tests/conftest.py index 6c45b574..d5bd090f 100644 --- a/sdks/python/tests/conftest.py +++ b/sdks/python/tests/conftest.py @@ -102,7 +102,7 @@ def unique_name() -> str: @pytest.fixture def test_agent_id() -> str: """Generate a unique agent ID for testing.""" - return f"test-agent-{uuid.uuid4().hex[:12]}" + return str(uuid.uuid4()) @pytest_asyncio.fixture diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py new file mode 100644 index 00000000..488538ac --- /dev/null +++ b/sdks/python/tests/test_agent_id_validation.py @@ -0,0 +1,76 @@ +"""SDK agent_id validation behavior tests.""" + +from unittest.mock import AsyncMock, MagicMock +from uuid import uuid4 + +import pytest + +from agent_control import agents, policies + + +class DummyResponse: + def raise_for_status(self) -> None: + return None + + def json(self) -> dict: + return {"ok": True} + + +@pytest.mark.asyncio +async def test_get_agent_rejects_invalid_uuid() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock() + + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + await agents.get_agent(client, "not-a-uuid") + + client.http_client.get.assert_not_called() + + +@pytest.mark.asyncio +async def test_get_agent_policy_rejects_invalid_uuid() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock() + + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + await agents.get_agent_policy(client, "not-a-uuid") + + client.http_client.get.assert_not_called() + + +@pytest.mark.asyncio +async def test_remove_agent_policy_rejects_invalid_uuid() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock() + + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + await agents.remove_agent_policy(client, "not-a-uuid") + + client.http_client.delete.assert_not_called() + + +@pytest.mark.asyncio +async def test_assign_policy_rejects_invalid_uuid() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock() + + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + await policies.assign_policy_to_agent(client, "not-a-uuid", policy_id=1) + + client.http_client.post.assert_not_called() + + +@pytest.mark.asyncio +async def test_get_agent_accepts_uuid_object() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) + + agent_id = uuid4() + await agents.get_agent(client, agent_id) + + client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_id}") diff --git a/sdks/python/tests/test_control_decorators.py b/sdks/python/tests/test_control_decorators.py index 762faef1..0d26d924 100644 --- a/sdks/python/tests/test_control_decorators.py +++ b/sdks/python/tests/test_control_decorators.py @@ -15,7 +15,7 @@ def mock_agent(): """Create a mock agent.""" agent = MagicMock() - agent.agent_id = "test-agent-uuid-123" + agent.agent_id = "550e8400-e29b-41d4-a716-446655440000" return agent diff --git a/sdks/python/tests/test_init_conflict.py b/sdks/python/tests/test_init_conflict.py new file mode 100644 index 00000000..3e712e01 --- /dev/null +++ b/sdks/python/tests/test_init_conflict.py @@ -0,0 +1,37 @@ +"""Tests that init() surfaces server-side conflicts.""" + +from unittest.mock import AsyncMock, patch +from uuid import uuid4 + +import httpx +import pytest + +import agent_control + + +def _make_conflict_error() -> httpx.HTTPStatusError: + request = httpx.Request("POST", "http://localhost:8000/api/v1/agents/initAgent") + response = httpx.Response(409, request=request) + return httpx.HTTPStatusError( + "Client error '409 Conflict' for url 'http://localhost:8000/api/v1/agents/initAgent'", + request=request, + response=response, + ) + + +def test_init_surfaces_uuid_conflict() -> None: + conflict = _make_conflict_error() + + with patch( + "agent_control.__init__.AgentControlClient.health_check", + new=AsyncMock(return_value={"status": "healthy"}), + ), patch( + "agent_control.__init__.agents.register_agent", + new=AsyncMock(side_effect=conflict), + ): + with pytest.raises(httpx.HTTPStatusError): + agent_control.init( + agent_name="Init Conflict Agent", + agent_id=str(uuid4()), + agent_description="Testing init conflict handling", + ) diff --git a/sdks/python/tests/test_init_validation.py b/sdks/python/tests/test_init_validation.py new file mode 100644 index 00000000..4d2c58b3 --- /dev/null +++ b/sdks/python/tests/test_init_validation.py @@ -0,0 +1,10 @@ +"""Validation tests for agent_control.init().""" + +import pytest + +import agent_control + + +def test_init_rejects_invalid_uuid() -> None: + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + agent_control.init(agent_name="Invalid UUID Agent", agent_id="not-a-uuid") diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index 35fab3b6..f3208a53 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -177,7 +177,7 @@ async def test_init_function_workflow( """ # Initialize agent agent = agent_control.init( - agent_name="Init Test Agent", + agent_name=f"Init Test Agent {test_agent_id}", agent_id=test_agent_id, agent_description="Testing init function", agent_version="1.0.0", @@ -189,7 +189,7 @@ async def test_init_function_workflow( # Verify agent instance assert agent is not None - assert agent.agent_name == "Init Test Agent" + assert agent.agent_name == f"Init Test Agent {test_agent_id}" assert hasattr(agent, "agent_id") # Verify current_agent() diff --git a/sdks/python/tests/test_validation.py b/sdks/python/tests/test_validation.py new file mode 100644 index 00000000..1564f78b --- /dev/null +++ b/sdks/python/tests/test_validation.py @@ -0,0 +1,42 @@ +"""Unit tests for SDK validation helpers.""" + +from uuid import UUID, uuid4 + +import pytest + +from agent_control.validation import ensure_uuid, ensure_uuid_str + + +def test_ensure_uuid_accepts_uuid_instance() -> None: + value = uuid4() + assert ensure_uuid(value) == value + + +def test_ensure_uuid_accepts_uuid_string() -> None: + value = uuid4() + assert ensure_uuid(str(value)) == value + + +def test_ensure_uuid_rejects_invalid_value() -> None: + with pytest.raises(ValueError, match="agent_id must be a valid UUID string"): + ensure_uuid("not-a-uuid") + + +def test_ensure_uuid_respects_field_name() -> None: + with pytest.raises(ValueError, match="agent_uuid must be a valid UUID string"): + ensure_uuid("not-a-uuid", field_name="agent_uuid") + + +def test_ensure_uuid_str_returns_string() -> None: + value = uuid4() + assert ensure_uuid_str(value) == str(value) + + +def test_ensure_uuid_str_accepts_uuid_string() -> None: + value = str(uuid4()) + assert ensure_uuid_str(value) == str(UUID(value)) + + +def test_ensure_uuid_str_rejects_invalid_value() -> None: + with pytest.raises(ValueError, match="agent_id must be a valid UUID string"): + ensure_uuid_str("not-a-uuid")