diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59b79f57..94e20531 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,7 +159,7 @@ sdks/python/src/agent_control/ import agent_control # Initialization -agent_control.init(agent_name="...", agent_id="...") +agent_control.init(agent_name="...", agent_name="...") # Decorator @agent_control.control() diff --git a/README.md b/README.md index af6a0b85..d0dff1eb 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ async def setup(): # 1. Register agent first (required before assigning policy) agent = Agent( # Your agent's UUID - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", agent_name="My Chatbot", agent_created_at=datetime.now(UTC).isoformat() ) @@ -165,7 +165,7 @@ async def setup(): # 5. Assign policy to agent await policies.assign_policy_to_agent( client, - agent_id=AGENT_ID, + agent_name=AGENT_ID, policy_id=policy["policy_id"] ) @@ -199,7 +199,7 @@ from agent_control import control, ControlViolationError # Initialize your agent agent_control.init( agent_name="My Chatbot", - agent_id="550e8400-e29b-41d4-a716-446655440000" + agent_name="550e8400-e29b-41d4-a716-446655440000" ) # Protect any function (like LLM calls) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index e41cee46..2b155237 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -587,7 +587,7 @@ import agent_control agent_control.init( agent_name="my-agent", # Required: human-readable name - agent_id="550e8400-e29b-41d4-a716-446655440000", # Required: UUID + agent_name="550e8400-e29b-41d4-a716-446655440000", # Required: UUID server_url="http://localhost:8000", # Optional: defaults to env var steps=[ # Optional: register available steps { @@ -770,12 +770,12 @@ Default: `http://localhost:8000/api/v1` |--------|----------|-------------| | `GET` | `/agents` | List all agents | | `POST` | `/agents/initAgent` | Register a new agent | -| `GET` | `/agents/{agent_id}` | Get agent details | -| `PATCH` | `/agents/{agent_id}` | Update agent | -| `GET` | `/agents/{agent_id}/controls` | List controls for agent | -| `GET` | `/agents/{agent_id}/policy` | Get agent's policy | -| `POST` | `/agents/{agent_id}/policy/{policy_id}` | Assign policy | -| `DELETE` | `/agents/{agent_id}/policy` | Remove policy | +| `GET` | `/agents/{agent_name}` | Get agent details | +| `PATCH` | `/agents/{agent_name}` | Update agent | +| `GET` | `/agents/{agent_name}/controls` | List controls for agent | +| `GET` | `/agents/{agent_name}/policy` | Get agent's policy | +| `POST` | `/agents/{agent_name}/policy/{policy_id}` | Assign policy | +| `DELETE` | `/agents/{agent_name}/policy` | Remove policy | **Controls**: diff --git a/docs/observability.md b/docs/observability.md index 96be1753..856bc7a9 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -125,7 +125,7 @@ class EventStore(ABC): @abstractmethod async def query_stats( - self, agent_uuid: UUID, time_range: timedelta, control_id: int | None = None + self, agent_name: UUID, time_range: timedelta, control_id: int | None = None ) -> StatsResult: """Query stats (aggregated at query time).""" pass @@ -220,12 +220,12 @@ Events are stored with minimal indexed columns + JSONB for flexibility: CREATE TABLE control_execution_events ( control_execution_id VARCHAR(36) PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL, - agent_uuid UUID NOT NULL, + agent_name UUID NOT NULL, data JSONB NOT NULL -- Full event stored here ); -- Primary index for time-range queries per agent -CREATE INDEX ix_events_agent_time ON control_execution_events (agent_uuid, timestamp DESC); +CREATE INDEX ix_events_agent_time ON control_execution_events (agent_name, timestamp DESC); -- Expression index for grouping by control CREATE INDEX ix_events_data_control_id ON control_execution_events ((data->>'control_id')); @@ -245,7 +245,7 @@ Each control evaluation produces an event (stored in the `data` JSONB column): control_execution_id: string, // Unique ID (for correlation) trace_id: string, // OpenTelemetry trace ID (32 hex chars) span_id: string, // OpenTelemetry span ID (16 hex chars) - agent_uuid: UUID, + agent_name: UUID, agent_name: string, control_id: number, control_name: string, @@ -272,9 +272,9 @@ All observability endpoints are under `/api/v1/observability/`. |----------|----------|------------|---------| | **Health check** | `GET /status` | — | System status | | **Ingest events** | `POST /events` | `events[]` in body | Ingestion result | -| **Agent overview** | `GET /stats` | `agent_uuid`, `time_range` | `totals` + `controls[]` | +| **Agent overview** | `GET /stats` | `agent_name`, `time_range` | `totals` + `controls[]` | | **Agent trends** | `GET /stats` | + `include_timeseries=true` | `totals.timeseries[]` included | -| **Control stats** | `GET /stats/controls/{id}` | `agent_uuid`, `time_range` | `control_id`, `control_name`, `stats` | +| **Control stats** | `GET /stats/controls/{id}` | `agent_name`, `time_range` | `control_id`, `control_name`, `stats` | | **Control trends** | `GET /stats/controls/{id}` | + `include_timeseries=true` | `stats.timeseries[]` included | | **Query raw events** | `POST /events/query` | Filters in body | `events[]` with pagination | @@ -323,7 +323,7 @@ Content-Type: application/json "control_execution_id": "...", "trace_id": "...", "span_id": "...", - "agent_uuid": "...", + "agent_name": "...", "control_id": 1, "control_name": "block-toxic", "matched": true, @@ -350,14 +350,14 @@ Content-Type: application/json Get agent-level aggregated statistics with per-control breakdown. ```http -GET /api/v1/observability/stats?agent_uuid=&time_range=&include_timeseries= +GET /api/v1/observability/stats?agent_name=&time_range=&include_timeseries= ``` **Query Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `agent_uuid` | UUID | Yes | Agent to get stats for | +| `agent_name` | UUID | Yes | Agent to get stats for | | `time_range` | string | No | Time range: `1m`, `5m`, `15m`, `1h`, `24h`, `7d`, `30d`, `180d`, `365d` (default: `5m`) | | `include_timeseries` | boolean | No | Include time-series data for trend visualization (default: `false`) | @@ -379,13 +379,13 @@ When `include_timeseries=true`, data is bucketed automatically based on the time **Example Request:** ```bash -curl "http://localhost:8000/api/v1/observability/stats?agent_uuid=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h" +curl "http://localhost:8000/api/v1/observability/stats?agent_name=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h" ``` **Example Response:** ```json { - "agent_uuid": "563de065-23aa-5d75-b594-cfa73abcc53c", + "agent_name": "563de065-23aa-5d75-b594-cfa73abcc53c", "time_range": "1h", "totals": { "execution_count": 8, @@ -442,13 +442,13 @@ curl "http://localhost:8000/api/v1/observability/stats?agent_uuid=563de065-23aa- **Example Request with Time-Series:** ```bash -curl "http://localhost:8000/api/v1/observability/stats?agent_uuid=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h&include_timeseries=true" +curl "http://localhost:8000/api/v1/observability/stats?agent_name=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h&include_timeseries=true" ``` **Example Response with Time-Series:** ```json { - "agent_uuid": "563de065-23aa-5d75-b594-cfa73abcc53c", + "agent_name": "563de065-23aa-5d75-b594-cfa73abcc53c", "time_range": "1h", "totals": { "execution_count": 8, @@ -512,7 +512,7 @@ Empty buckets are included with zero counts and `null` averages to ensure consis Get statistics for a single control. ```http -GET /api/v1/observability/stats/controls/{control_id}?agent_uuid=&time_range=&include_timeseries= +GET /api/v1/observability/stats/controls/{control_id}?agent_name=&time_range=&include_timeseries= ``` **Path Parameters:** @@ -525,19 +525,19 @@ GET /api/v1/observability/stats/controls/{control_id}?agent_uuid=&time_ran | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `agent_uuid` | UUID | Yes | Agent to get stats for | +| `agent_name` | UUID | Yes | Agent to get stats for | | `time_range` | string | No | Time range: `1m`, `5m`, `15m`, `1h`, `24h`, `7d`, `30d`, `180d`, `365d` (default: `5m`) | | `include_timeseries` | boolean | No | Include time-series data for trend visualization (default: `false`) | **Example Request:** ```bash -curl "http://localhost:8000/api/v1/observability/stats/controls/1?agent_uuid=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h&include_timeseries=true" +curl "http://localhost:8000/api/v1/observability/stats/controls/1?agent_name=563de065-23aa-5d75-b594-cfa73abcc53c&time_range=1h&include_timeseries=true" ``` **Example Response:** ```json { - "agent_uuid": "563de065-23aa-5d75-b594-cfa73abcc53c", + "agent_name": "563de065-23aa-5d75-b594-cfa73abcc53c", "time_range": "1h", "control_id": 1, "control_name": "block-prompt-injection", @@ -595,7 +595,7 @@ Content-Type: application/json | `trace_id` | string | No | Filter by trace ID | | `span_id` | string | No | Filter by span ID | | `control_execution_id` | string | No | Get specific event | -| `agent_uuid` | UUID | No | Filter by agent | +| `agent_name` | UUID | No | Filter by agent | | `control_ids` | integer[] | No | Filter by control IDs | | `actions` | string[] | No | Filter by actions: `allow`, `deny`, `warn`, `log` | | `matched` | boolean | No | Filter by matched status | @@ -611,7 +611,7 @@ Content-Type: application/json curl -X POST "http://localhost:8000/api/v1/observability/events/query" \ -H "Content-Type: application/json" \ -d '{ - "agent_uuid": "563de065-23aa-5d75-b594-cfa73abcc53c", + "agent_name": "563de065-23aa-5d75-b594-cfa73abcc53c", "matched": true, "limit": 5 }' @@ -625,7 +625,7 @@ curl -X POST "http://localhost:8000/api/v1/observability/events/query" \ "control_execution_id": "92df0332-170c-4bc6-aefd-ab50be311062", "trace_id": "5848335875e1d7269e148170ccb617ca", "span_id": "c25549deddcaecbe", - "agent_uuid": "563de065-23aa-5d75-b594-cfa73abcc53c", + "agent_name": "563de065-23aa-5d75-b594-cfa73abcc53c", "agent_name": "Customer Support Agent", "control_id": 3, "control_name": "block-credit-card", diff --git a/docs/testing.md b/docs/testing.md index 16895487..5d6d3840 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -91,7 +91,7 @@ async def test_sdk_denies_on_local_control() -> None: # When: evaluating via the SDK public API result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="db_query", input={"sql": "SELECT 1"}, output=None), stage="pre", controls=controls, diff --git a/engine/tests/test_core.py b/engine/tests/test_core.py index dad8fcbe..46e25eee 100644 --- a/engine/tests/test_core.py +++ b/engine/tests/test_core.py @@ -236,7 +236,7 @@ async def test_parallel_evaluation_starts_all_controls(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -268,7 +268,7 @@ async def test_parallel_evaluation_faster_than_sequential(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -303,7 +303,7 @@ async def test_cancel_on_deny_cancels_blocking_tasks(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -338,7 +338,7 @@ async def test_cancel_on_deny_with_multiple_blockers(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -364,7 +364,7 @@ async def test_no_cancel_on_non_deny_match(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -390,7 +390,7 @@ async def test_first_deny_wins(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -424,7 +424,7 @@ async def test_collect_all_completed_results(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -447,7 +447,7 @@ async def test_no_matches_when_all_allow(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -535,7 +535,7 @@ async def test_evaluator_error_fails_closed_for_deny(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -571,7 +571,7 @@ async def test_error_does_not_affect_other_controls(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -609,7 +609,7 @@ async def test_error_with_log_action_fails_open(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -641,7 +641,7 @@ async def test_missing_evaluator_error_sets_error_field(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -678,7 +678,7 @@ async def test_errors_array_exposes_evaluator_failures(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -724,7 +724,7 @@ async def test_errors_array_empty_when_no_errors(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -765,7 +765,7 @@ async def test_confidence_is_full_on_deny_match(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -799,7 +799,7 @@ async def test_confidence_excludes_cancelled_tasks(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -831,7 +831,7 @@ async def test_confidence_proportional_without_deny_match(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -860,7 +860,7 @@ async def test_confidence_zero_when_deny_errors_despite_other_successes(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -949,7 +949,7 @@ async def test_step_names_filters_tasks(self): ] engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="tool", name="copy_file", input={}, output=None), stage="pre", ) @@ -986,7 +986,7 @@ async def test_step_name_regex_filters_tasks(self): ] engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="tool", name="db_query", input={}, output=None), stage="pre", ) @@ -1014,7 +1014,7 @@ async def test_or_semantics_names_or_regex(self): engine = ControlEngine(controls) # Matches by regex despite name mismatch request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="tool", name="db_export", input={}, output=None), stage="pre", ) @@ -1039,7 +1039,7 @@ async def test_path_optional_defaults_to_star(self): ] engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="tool", name="copy_file", input={}, output=None), stage="pre", ) @@ -1104,7 +1104,7 @@ async def test_evaluator_timeout_is_enforced(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1162,7 +1162,7 @@ async def test_timeout_does_not_affect_fast_evaluators(self): # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1247,7 +1247,7 @@ async def evaluate(self, data: Any) -> EvaluatorResult: # When: Processing request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1327,7 +1327,7 @@ async def test_server_context_only_runs_server_controls(self): engine = ControlEngine(controls, context="server") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1357,7 +1357,7 @@ async def test_sdk_context_only_runs_sdk_controls(self): engine = ControlEngine(controls, context="sdk") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1387,7 +1387,7 @@ async def test_default_context_is_server(self): engine = ControlEngine(controls) # No context param request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1417,7 +1417,7 @@ async def test_sdk_context_empty_when_no_local_controls(self): engine = ControlEngine(controls, context="sdk") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1449,7 +1449,7 @@ async def test_server_context_empty_when_all_local_controls(self): engine = ControlEngine(controls, context="server") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1478,7 +1478,7 @@ async def test_sdk_deny_works_in_sdk_context(self): engine = ControlEngine(controls, context="sdk") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="llm", name="test-step", input="test", output=None), stage="pre", ) @@ -1522,7 +1522,7 @@ async def test_context_filtering_combined_with_step_scoping(self): engine = ControlEngine(controls, context="sdk") request = EvaluationRequest( - agent_uuid="00000000-0000-0000-0000-000000000001", + agent_name="00000000-0000-0000-0000-000000000001", step=Step(type="tool", name="copy_file", input={}, output=None), stage="pre", ) diff --git a/examples/README.md b/examples/README.md index da4e369f..48f968c9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -99,7 +99,7 @@ from agent_control import control, ControlViolationError # Initialize agent (connects to server, loads policy) agent_control.init( agent_name="my-bot", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", ) # Apply the agent's assigned policy diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 8a1e7cfe..f0fd4e10 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -161,8 +161,7 @@ async def run_demo(): try: logger.info(f"Initializing agent: {AGENT_NAME}") agent_control.init( - agent_name=AGENT_NAME, - agent_id=AGENT_ID, + agent_name=AGENT_ID, server_url=SERVER_URL, agent_description="Demo chatbot for testing controls" ) diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index cf4b6c73..658d0dda 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -45,14 +45,14 @@ async def create_agent(client: AgentControlClient) -> str: print("=" * 60) # Use the provided UUID for the agent - agent_uuid = UUID(AGENT_ID) + agent_name = UUID(AGENT_ID) try: response = await client.http_client.post( "/api/v1/agents/initAgent", # Correct endpoint json={ "agent": { - "agent_id": str(agent_uuid), + "agent_name": str(agent_name), "agent_name": AGENT_NAME, "agent_description": "Demo chatbot for testing controls", }, @@ -69,8 +69,8 @@ async def create_agent(client: AgentControlClient) -> str: else: print(f"✓ Agent already exists: {AGENT_NAME}") - print(f" Agent UUID: {agent_uuid}") - return str(agent_uuid) + print(f" Agent UUID: {agent_name}") + return str(agent_name) except Exception as e: print(f"✗ Failed to create agent: {e}") @@ -234,7 +234,7 @@ async def add_control_to_policy( async def assign_policy_to_agent( client: AgentControlClient, - agent_uuid: str, + agent_name: str, policy_id: int ) -> bool: """Assign a policy to an agent.""" @@ -244,11 +244,11 @@ async def assign_policy_to_agent( try: response = await client.http_client.post( - f"/api/v1/agents/{agent_uuid}/policy/{policy_id}" + f"/api/v1/agents/{agent_name}/policy/{policy_id}" ) response.raise_for_status() data = response.json() - print(f"✓ Assigned policy {policy_id} to agent {agent_uuid}") + print(f"✓ Assigned policy {policy_id} to agent {agent_name}") print(f" Response: {data}") return True except Exception as e: @@ -256,7 +256,7 @@ async def assign_policy_to_agent( return False -async def list_agent_controls(client: AgentControlClient, agent_uuid: str) -> list: +async def list_agent_controls(client: AgentControlClient, agent_name: str) -> list: """List all controls for the agent.""" print("\n" + "=" * 60) print("STEP 6: Listing Agent's Controls") @@ -264,7 +264,7 @@ async def list_agent_controls(client: AgentControlClient, agent_uuid: str) -> li try: response = await client.http_client.get( - f"/api/v1/agents/{agent_uuid}/controls" + f"/api/v1/agents/{agent_name}/controls" ) response.raise_for_status() data = response.json() @@ -363,7 +363,7 @@ async def get_control_data(client: AgentControlClient, control_id: int) -> dict: raise -async def verify_full_chain(client: AgentControlClient, agent_uuid: str) -> None: +async def verify_full_chain(client: AgentControlClient, agent_name: str) -> None: """Debug function to verify the entire chain.""" print("\n" + "=" * 60) print("DEBUG: Verifying Full Chain") @@ -372,7 +372,7 @@ async def verify_full_chain(client: AgentControlClient, agent_uuid: str) -> None # 1. Get agent info print("\n1. Agent Info:") try: - resp = await client.http_client.get(f"/api/v1/agents/{agent_uuid}") + resp = await client.http_client.get(f"/api/v1/agents/{agent_name}") resp.raise_for_status() agent_data = resp.json() print(f" Agent: {agent_data}") @@ -382,7 +382,7 @@ async def verify_full_chain(client: AgentControlClient, agent_uuid: str) -> None # 2. Get agent's policy print("\n2. Agent's Policy:") try: - resp = await client.http_client.get(f"/api/v1/agents/{agent_uuid}/policy") + resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/policy") if resp.status_code == 404: print(" No policy assigned to agent") policy_id = None @@ -407,7 +407,7 @@ async def verify_full_chain(client: AgentControlClient, agent_uuid: str) -> None # 4. Final: List agent controls (the API we're testing) print("\n4. Final Agent Controls (via /agents/{id}/controls):") try: - resp = await client.http_client.get(f"/api/v1/agents/{agent_uuid}/controls") + resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/controls") resp.raise_for_status() controls = resp.json() print(f" Controls: {controls}") @@ -440,15 +440,15 @@ async def main(): return # Use the provided UUID for verification - agent_uuid = AGENT_ID + agent_name = AGENT_ID # If verify-only mode, just run verification if args.verify_only: - await verify_full_chain(client, agent_uuid) + await verify_full_chain(client, agent_name) return # 1. Create agent - agent_uuid = await create_agent(client) + agent_name = await create_agent(client) # 2. Create controls regex_control_id = await create_regex_control(client) @@ -457,14 +457,14 @@ async def main(): # Skip remaining steps if controls already existed if regex_control_id == -1 or list_control_id == -1: print("\n⚠️ Some controls already exist. Running verification...") - await verify_full_chain(client, agent_uuid) + await verify_full_chain(client, agent_name) return # 3. Create policy policy_id = await create_policy(client, "demo-policy") if policy_id == -1: print("\n⚠️ Policy already exists. Running verification...") - await verify_full_chain(client, agent_uuid) + await verify_full_chain(client, agent_name) return # 4. Add controls to policy @@ -487,7 +487,7 @@ async def main(): print(f" Failed to verify policy: {e}") # 5. Assign policy to agent - ok3 = await assign_policy_to_agent(client, agent_uuid, policy_id) + ok3 = await assign_policy_to_agent(client, agent_name, policy_id) if not ok3: print("\n⚠️ Failed to assign policy to agent!") @@ -495,7 +495,7 @@ async def main(): print("\n Verifying agent policy assignment...") try: resp = await client.http_client.get( - f"/api/v1/agents/{agent_uuid}/policy" + f"/api/v1/agents/{agent_name}/policy" ) resp.raise_for_status() agent_policy = resp.json() @@ -508,7 +508,7 @@ async def main(): print(f" ✗ Failed to verify agent policy: {e}") # 6. List controls - await list_agent_controls(client, agent_uuid) + await list_agent_controls(client, agent_name) # 7. Update the list control await update_control(client, list_control_id) diff --git a/examples/agent_control_demo/update_controls.py b/examples/agent_control_demo/update_controls.py index 6c7ab3f9..ebd1e25a 100644 --- a/examples/agent_control_demo/update_controls.py +++ b/examples/agent_control_demo/update_controls.py @@ -29,10 +29,10 @@ SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") -async def get_control_by_name(client: AgentControlClient, agent_uuid: str, name: str) -> dict | None: +async def get_control_by_name(client: AgentControlClient, agent_name: str, name: str) -> dict | None: """Find a control by name from the agent's controls.""" try: - response = await client.http_client.get(f"/api/v1/agents/{agent_uuid}/controls") + response = await client.http_client.get(f"/api/v1/agents/{agent_name}/controls") response.raise_for_status() controls = response.json().get("controls", []) @@ -126,13 +126,13 @@ async def block_ssn(client: AgentControlClient, control_id: int) -> None: raise -async def show_current_status(client: AgentControlClient, agent_uuid: str) -> None: +async def show_current_status(client: AgentControlClient, agent_name: str) -> None: """Show the current status of the SSN control.""" print("\n" + "=" * 60) print("Current SSN Control Status") print("=" * 60) - ctrl = await get_control_by_name(client, agent_uuid, "block-ssn-output") + ctrl = await get_control_by_name(client, agent_name, "block-ssn-output") if ctrl: ctrl_def = ctrl.get("control", {}) enabled = ctrl_def.get("enabled", True) @@ -155,13 +155,13 @@ async def main(): args = parser.parse_args() # Use the provided UUID - agent_uuid = AGENT_ID + agent_name = AGENT_ID print("\n" + "=" * 60) print("AGENT CONTROL DEMO: Update Controls") print("=" * 60) print(f"\nServer URL: {SERVER_URL}") - print(f"Agent UUID: {agent_uuid}") + print(f"Agent UUID: {agent_name}") async with AgentControlClient(base_url=SERVER_URL) as client: # Check server health @@ -174,7 +174,7 @@ async def main(): return # Find the SSN control - ctrl = await get_control_by_name(client, agent_uuid, "block-ssn-output") + ctrl = await get_control_by_name(client, agent_name, "block-ssn-output") if not ctrl: print("\n✗ SSN control not found!") print(" Run setup_controls.py first:") @@ -184,16 +184,16 @@ async def main(): control_id = ctrl.get("id") if args.status: - await show_current_status(client, agent_uuid) + await show_current_status(client, agent_name) elif args.allow_ssn: await allow_ssn(client, control_id) - await show_current_status(client, agent_uuid) + await show_current_status(client, agent_name) elif args.block_ssn: await block_ssn(client, control_id) - await show_current_status(client, agent_uuid) + await show_current_status(client, agent_name) else: # Default: show status and usage - await show_current_status(client, agent_uuid) + await show_current_status(client, agent_name) print("\n" + "=" * 60) print("Usage") print("=" * 60) diff --git a/examples/crewai/content_agent_protection.py b/examples/crewai/content_agent_protection.py index 118be45a..2827d4e3 100644 --- a/examples/crewai/content_agent_protection.py +++ b/examples/crewai/content_agent_protection.py @@ -53,8 +53,7 @@ server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") agent_control.init( - agent_name=AGENT_NAME, - agent_id=AGENT_ID, + agent_name=AGENT_ID, agent_description=AGENT_DESCRIPTION, server_url=server_url, ) diff --git a/examples/crewai/setup_content_controls.py b/examples/crewai/setup_content_controls.py index 94674e4d..779ae814 100644 --- a/examples/crewai/setup_content_controls.py +++ b/examples/crewai/setup_content_controls.py @@ -10,7 +10,6 @@ import asyncio import os -from uuid import UUID from agent_control import Agent, AgentControlClient, agents, controls, policies @@ -22,11 +21,10 @@ 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(AGENT_ID) + agent_name = AGENT_ID agent = Agent( - agent_id=agent_uuid, - agent_name="Customer Support Crew", + agent_name=agent_name, agent_description="Customer support crew with PII protection and access controls" ) @@ -226,7 +224,7 @@ async def setup_content_controls(): # 8. Assign Policy to Agent try: - await policies.assign_policy_to_agent(client, agent_uuid, policy_id) + await policies.assign_policy_to_agent(client, agent_name, policy_id) print(f"✓ Assigned policy to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): diff --git a/examples/customer_support_agent/README.md b/examples/customer_support_agent/README.md index 8eeedd49..30571390 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="646d5dea-c2e6-4453-b446-7035482b38e4", + agent_name="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 859c338a..f71c0630 100755 --- a/examples/customer_support_agent/demo.sh +++ b/examples/customer_support_agent/demo.sh @@ -228,7 +228,7 @@ get_agent_uuid() { } show_observability_stats() { - local agent_uuid=$(get_agent_uuid) + local agent_name=$(get_agent_uuid) local server_url="${AGENT_CONTROL_URL:-http://localhost:8000}" echo "" @@ -246,7 +246,7 @@ show_observability_stats() { # Fetch stats for 5 minutes echo "Last 5 minutes:" echo "---------------" - local stats_5m=$(curl -s "${server_url}/api/v1/observability/stats?agent_uuid=${agent_uuid}&time_range=5m") + local stats_5m=$(curl -s "${server_url}/api/v1/observability/stats?agent_name=${agent_name}&time_range=5m") if echo "$stats_5m" | python3 -c "import sys, json; d=json.load(sys.stdin); print(f\" Total executions: {d.get('total_executions', 0)}\"); print(f\" Matches: {d.get('total_matches', 0)}\"); print(f\" Non-matches: {d.get('total_non_matches', 0)}\"); print(f\" Errors: {d.get('total_errors', 0)}\"); actions=d.get('action_counts', {}); print(f\" Actions: allow={actions.get('allow', 0)}, deny={actions.get('deny', 0)}, warn={actions.get('warn', 0)}, log={actions.get('log', 0)}\")" 2>/dev/null; then : else @@ -258,7 +258,7 @@ show_observability_stats() { # Fetch stats for 1 hour (closest to 30 mins available) echo "Last 1 hour:" echo "------------" - local stats_1h=$(curl -s "${server_url}/api/v1/observability/stats?agent_uuid=${agent_uuid}&time_range=1h") + local stats_1h=$(curl -s "${server_url}/api/v1/observability/stats?agent_name=${agent_name}&time_range=1h") if echo "$stats_1h" | python3 -c "import sys, json; d=json.load(sys.stdin); print(f\" Total executions: {d.get('total_executions', 0)}\"); print(f\" Matches: {d.get('total_matches', 0)}\"); print(f\" Non-matches: {d.get('total_non_matches', 0)}\"); print(f\" Errors: {d.get('total_errors', 0)}\"); actions=d.get('action_counts', {}); print(f\" Actions: allow={actions.get('allow', 0)}, deny={actions.get('deny', 0)}, warn={actions.get('warn', 0)}, log={actions.get('log', 0)}\")" 2>/dev/null; then : else diff --git a/examples/customer_support_agent/run_demo.py b/examples/customer_support_agent/run_demo.py index 84eac08a..34ad71ef 100644 --- a/examples/customer_support_agent/run_demo.py +++ b/examples/customer_support_agent/run_demo.py @@ -60,17 +60,17 @@ async def reset_agent(): """Reset the agent by removing its policy (which disconnects all controls).""" - agent_uuid = AGENT_ID + agent_name = AGENT_ID server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") - logger.info(f"Resetting agent '{AGENT_ID}' (UUID: {agent_uuid})") - print(f"Resetting agent '{AGENT_ID}' (UUID: {agent_uuid})...") + logger.info(f"Resetting agent '{AGENT_ID}' (UUID: {agent_name})") + print(f"Resetting agent '{AGENT_ID}' (UUID: {agent_name})...") print() async with AgentControlClient(base_url=server_url) as client: # Check if agent exists try: - await agents.get_agent(client, agent_uuid) + await agents.get_agent(client, agent_name) logger.debug("Agent exists, proceeding with reset") except Exception as e: if "404" in str(e): @@ -83,7 +83,7 @@ async def reset_agent(): # Remove policy from agent (disconnects all controls) try: - await agents.remove_agent_policy(client, agent_uuid) + await agents.remove_agent_policy(client, agent_name) logger.info("Successfully removed policy from agent") print("Removed policy from agent (all controls disconnected).") except Exception as e: diff --git a/examples/customer_support_agent/setup_demo_controls.py b/examples/customer_support_agent/setup_demo_controls.py index 904f38ce..bce590fe 100644 --- a/examples/customer_support_agent/setup_demo_controls.py +++ b/examples/customer_support_agent/setup_demo_controls.py @@ -226,8 +226,8 @@ async def setup_demo(quiet: bool = False): """Set up the demo agent with controls.""" - # Use the provided UUID (must match support_agent.py) - agent_uuid = AGENT_ID + # Use the provided agent identifier (must match support_agent.py) + agent_name = AGENT_ID async with AgentControlClient(base_url=SERVER_URL, timeout=30.0) as client: # Check server health @@ -242,8 +242,7 @@ async def setup_demo(quiet: bool = False): # Register the agent try: agent = Agent( - agent_id=agent_uuid, - agent_name=AGENT_NAME, + agent_name=agent_name, agent_description=AGENT_DESCRIPTION, ) result = await agents.register_agent(client, agent, steps=[]) @@ -259,7 +258,7 @@ async def setup_demo(quiet: bool = False): # Check if agent already has a policy try: - policy_info = await agents.get_agent_policy(client, agent_uuid) + policy_info = await agents.get_agent_policy(client, agent_name) policy_id = policy_info.get("policy_id") except Exception: policy_id = None # No policy yet @@ -282,7 +281,7 @@ async def setup_demo(quiet: bool = False): return False try: - await policies.assign_policy_to_agent(client, agent_uuid, policy_id) + await policies.assign_policy_to_agent(client, agent_name, policy_id) except Exception as e: print(f" Error assigning policy: {e}") return False diff --git a/examples/customer_support_agent/support_agent.py b/examples/customer_support_agent/support_agent.py index 0cd7a876..82273005 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -29,8 +29,7 @@ # The agent registers with the server and loads its assigned policy. agent_control.init( - agent_name="Customer Support Agent", - agent_id="646d5dea-c2e6-4453-b446-7035482b38e4", + agent_name="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/deepeval/qa_agent.py b/examples/deepeval/qa_agent.py index 63a205f7..b4552326 100755 --- a/examples/deepeval/qa_agent.py +++ b/examples/deepeval/qa_agent.py @@ -37,8 +37,7 @@ # ============================================================================= agent_control.init( - agent_name="Q&A Agent with DeepEval", - agent_id="qa-agent-deepeval", + agent_name="qa-agent-deepeval", agent_description="Question answering agent with DeepEval quality controls", agent_version="1.0.0", ) diff --git a/examples/deepeval/setup_controls.py b/examples/deepeval/setup_controls.py index ec1f37da..f3178cb5 100755 --- a/examples/deepeval/setup_controls.py +++ b/examples/deepeval/setup_controls.py @@ -149,11 +149,11 @@ async def setup_demo(quiet: bool = False): """Set up the demo agent with DeepEval controls.""" # Generate the same UUID5 that the SDK generates - agent_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + agent_name = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) print(f"Setting up agent: {AGENT_NAME}") print(f"Agent ID: {AGENT_ID}") - print(f"Agent UUID: {agent_uuid}") + print(f"Agent UUID: {agent_name}") print(f"Server URL: {SERVER_URL}") print() @@ -175,7 +175,7 @@ async def setup_demo(quiet: bool = False): "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_uuid, + "agent_name": agent_name, "agent_name": AGENT_NAME, "agent_description": AGENT_DESCRIPTION, }, @@ -196,7 +196,7 @@ async def setup_demo(quiet: bool = False): # Check if agent already has a policy try: - resp = await client.get(f"/api/v1/agents/{agent_uuid}/policy") + resp = await client.get(f"/api/v1/agents/{agent_name}/policy") if resp.status_code == 200: policy_id = resp.json().get("policy_id") print(f"✓ Found existing policy: {policy_id}") @@ -224,7 +224,7 @@ async def setup_demo(quiet: bool = False): print(f"✓ Created policy: {policy_name}") # Assign policy to agent - resp = await client.post(f"/api/v1/agents/{agent_uuid}/policy/{policy_id}") + resp = await client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") resp.raise_for_status() print(f"✓ Assigned policy to agent") except httpx.HTTPError as e: diff --git a/examples/langchain/langgraph_auto_schema_agent.py b/examples/langchain/langgraph_auto_schema_agent.py index 7a6e1c3e..b077d0df 100644 --- a/examples/langchain/langgraph_auto_schema_agent.py +++ b/examples/langchain/langgraph_auto_schema_agent.py @@ -241,8 +241,7 @@ async def main() -> None: """Run the demo end-to-end.""" print("Initializing Agent Control (no explicit steps passed)...") agent_control.init( - agent_name=AGENT_NAME, - agent_id=AGENT_ID, + agent_name=AGENT_ID, agent_description=AGENT_DESCRIPTION, server_url=os.getenv("AGENT_CONTROL_URL"), ) diff --git a/examples/langchain/setup_sql_controls.py b/examples/langchain/setup_sql_controls.py index 59bafda2..28512907 100644 --- a/examples/langchain/setup_sql_controls.py +++ b/examples/langchain/setup_sql_controls.py @@ -13,7 +13,6 @@ import asyncio import os import pathlib -from uuid import UUID import requests @@ -52,11 +51,10 @@ 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(AGENT_ID) + agent_name = AGENT_ID agent = Agent( - agent_id=agent_uuid, - agent_name="SQL Demo Agent", + agent_name=agent_name, agent_description="SQL agent with server-side controls" ) @@ -141,7 +139,7 @@ async def setup_sql_controls(): if "409" in str(e): print("ℹ️ Policy 'sql-protection-policy' already exists, checking agent...") try: - policy_info = await agents.get_agent_policy(client, str(agent_uuid)) + policy_info = await agents.get_agent_policy(client, str(agent_name)) policy_id = policy_info.get("policy_id") if policy_id is None: raise ValueError("No policy assigned to agent.") @@ -172,7 +170,7 @@ async def setup_sql_controls(): # 5. Assign Policy to Agent try: - await policies.assign_policy_to_agent(client, agent_uuid, policy_id) + await policies.assign_policy_to_agent(client, agent_name, policy_id) print(f"✓ Assigned policy to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): diff --git a/examples/langchain/sql_agent_protection.py b/examples/langchain/sql_agent_protection.py index 3e6dfb08..b0fd84f6 100644 --- a/examples/langchain/sql_agent_protection.py +++ b/examples/langchain/sql_agent_protection.py @@ -131,7 +131,7 @@ async def safe_query_tool(query: str): async with AgentControlClient() as client: result = await check_evaluation_with_local( client=client, - agent_uuid=agent.agent_id, + agent_name=agent.agent_name, step=step, stage="pre", controls=local_controls, @@ -210,8 +210,7 @@ async def main(): print("Initializing SQL Agent...") agent_control.init( - agent_name=AGENT_NAME, - agent_id=AGENT_ID, + agent_name=AGENT_ID, agent_description=AGENT_DESCRIPTION, server_url=os.getenv("AGENT_CONTROL_URL"), ) diff --git a/models/README.md b/models/README.md index cfb52396..fabc69e8 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="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries", agent_version="1.0.0" ) @@ -90,7 +90,7 @@ from agent_control_models import EvaluationRequest, EvaluationResponse # Create evaluation request request = EvaluationRequest( - agent_uuid="agent-uuid-here", + agent_name="agent-uuid-here", step=Step( type="llm_inference", name="chat", @@ -129,7 +129,7 @@ Agent metadata and configuration. **Fields:** - `agent_name` (str): Human-readable agent name -- `agent_id` (UUID): Unique identifier +- `agent_name` (UUID): Unique identifier - `agent_description` (Optional[str]): Agent description - `agent_version` (Optional[str]): Agent version - `tools` (Optional[List[str]]): List of available tools @@ -165,7 +165,7 @@ Complete control specification. Request for evaluating controls. **Fields:** -- `agent_uuid` (str): Agent identifier +- `agent_name` (str): Agent identifier - `step` (Step): Step to evaluate - `stage` (str): Evaluation stage ("pre" or "post") @@ -198,7 +198,7 @@ from agent_control_models import Agent # Create with validation agent = Agent( agent_name="My Agent", - agent_id="550e8400-e29b-41d4-a716-446655440000" + agent_name="550e8400-e29b-41d4-a716-446655440000" ) # Serialize to dict @@ -227,7 +227,7 @@ step = Step( # Type-safe evaluation request request = EvaluationRequest( - agent_uuid="uuid-here", + agent_name="uuid-here", step=step, stage="pre" ) @@ -243,7 +243,7 @@ from agent_control_models import Agent # Add custom metadata agent = Agent( agent_name="Support Bot", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", metadata={ "team": "customer-success", "environment": "production", diff --git a/models/src/agent_control_models/agent.py b/models/src/agent_control_models/agent.py index 785039b4..6a0eedba 100644 --- a/models/src/agent_control_models/agent.py +++ b/models/src/agent_control_models/agent.py @@ -1,8 +1,8 @@ """Agent entity and step models.""" from __future__ import annotations +import re from typing import Any -from uuid import UUID from pydantic import Field, field_validator, model_validator @@ -15,19 +15,37 @@ STEP_TYPE_LLM = "llm" BUILTIN_STEP_TYPES: tuple[str, str] = (STEP_TYPE_TOOL, STEP_TYPE_LLM) +AGENT_NAME_MIN_LENGTH = 10 +AGENT_NAME_PATTERN = r"^[a-z0-9:_-]+$" +_AGENT_NAME_REGEX = re.compile(AGENT_NAME_PATTERN) + + +def normalize_agent_name(value: str) -> str: + """Normalize and validate an agent identifier.""" + normalized = value.strip().lower() + if len(normalized) < AGENT_NAME_MIN_LENGTH: + raise ValueError( + f"agent_name must be at least {AGENT_NAME_MIN_LENGTH} characters long" + ) + if not _AGENT_NAME_REGEX.fullmatch(normalized): + raise ValueError( + "agent_name may only contain lowercase letters, digits, ':', '_' or '-'" + ) + return normalized + class Agent(BaseModel): """ Agent metadata for registration and tracking. An agent represents an AI system that can be protected and monitored. - Each agent has a unique ID and can have multiple steps registered with it. + Each agent has a unique immutable name and can have multiple steps registered with it. """ - agent_id: UUID = Field( - ..., description="Unique identifier for the agent (UUID format)" - ) agent_name: str = Field( - ..., description="Human-readable name for the agent", min_length=1 + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Unique immutable identifier for the agent", ) agent_description: str | None = Field( None, description="Optional description of the agent's purpose" @@ -49,7 +67,6 @@ class Agent(BaseModel): "json_schema_extra": { "examples": [ { - "agent_id": "550e8400-e29b-41d4-a716-446655440000", "agent_name": "customer-service-bot", "agent_description": "Handles customer inquiries and support tickets", "agent_version": "1.0.0", @@ -59,6 +76,11 @@ class Agent(BaseModel): } } + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) + class StepSchema(BaseModel): """Schema for a registered agent step.""" diff --git a/models/src/agent_control_models/errors.py b/models/src/agent_control_models/errors.py index 2022a271..6ee41c42 100644 --- a/models/src/agent_control_models/errors.py +++ b/models/src/agent_control_models/errors.py @@ -12,8 +12,8 @@ "type": "https://agent-control.dev/errors/not-found", "title": "Resource Not Found", "status": 404, - "detail": "Agent with ID '550e8400-e29b-41d4-a716-446655440000' not found", - "instance": "/api/v1/agents/550e8400-e29b-41d4-a716-446655440000", + "detail": "Agent with name 'customer-service-bot' not found", + "instance": "/api/v1/agents/customer-service-bot", "error_code": "AGENT_NOT_FOUND", "kind": "Status", "api_version": "v1", @@ -25,9 +25,9 @@ "errors": [ { "resource": "Agent", - "field": "agent_id", + "field": "agent_name", "code": "not_found", - "message": "Agent with ID '550e8400-e29b-41d4-a716-446655440000' does not exist" + "message": "Agent with name 'customer-service-bot' does not exist" } ] } @@ -65,7 +65,6 @@ class ErrorCode(StrEnum): # Conflict Errors (3xx pattern) AGENT_NAME_CONFLICT = "AGENT_NAME_CONFLICT" - AGENT_UUID_CONFLICT = "AGENT_UUID_CONFLICT" POLICY_NAME_CONFLICT = "POLICY_NAME_CONFLICT" CONTROL_NAME_CONFLICT = "CONTROL_NAME_CONFLICT" EVALUATOR_NAME_CONFLICT = "EVALUATOR_NAME_CONFLICT" @@ -281,8 +280,8 @@ class ProblemDetail(BaseModel): "type": "https://agent-control.dev/errors/not-found", "title": "Resource Not Found", "status": 404, - "detail": "Agent with ID '550e8400-e29b-41d4-a716-446655440000' not found", - "instance": "/api/v1/agents/550e8400-e29b-41d4-a716-446655440000", + "detail": "Agent with name 'customer-service-bot' not found", + "instance": "/api/v1/agents/customer-service-bot", "error_code": "AGENT_NOT_FOUND", "kind": "Status", "api_version": "v1", @@ -356,7 +355,6 @@ def make_error_type(error_code: ErrorCode) -> str: ErrorCode.EVALUATOR_CONFIG_NOT_FOUND: "Evaluator Config Not Found", # Conflict errors ErrorCode.AGENT_NAME_CONFLICT: "Agent Name Already Exists", - ErrorCode.AGENT_UUID_CONFLICT: "Agent UUID Conflict", ErrorCode.POLICY_NAME_CONFLICT: "Policy Name Already Exists", ErrorCode.CONTROL_NAME_CONFLICT: "Control Name Already Exists", ErrorCode.EVALUATOR_NAME_CONFLICT: "Evaluator Name Conflict", diff --git a/models/src/agent_control_models/evaluation.py b/models/src/agent_control_models/evaluation.py index d22b44a9..07ab4810 100644 --- a/models/src/agent_control_models/evaluation.py +++ b/models/src/agent_control_models/evaluation.py @@ -1,10 +1,9 @@ """Evaluation-related models.""" from typing import Literal -from uuid import UUID -from pydantic import Field +from pydantic import Field, field_validator -from .agent import Step +from .agent import AGENT_NAME_MIN_LENGTH, AGENT_NAME_PATTERN, Step, normalize_agent_name from .base import BaseModel from .controls import ControlMatch @@ -17,12 +16,15 @@ class EvaluationRequest(BaseModel): policy compliance, and control rules. Attributes: - agent_uuid: UUID of the agent making the request + agent_name: Unique identifier of the agent making the request step: Step payload for evaluation stage: 'pre' (before execution) or 'post' (after execution) """ - agent_uuid: UUID = Field( - ..., description="UUID of the agent making the evaluation request" + agent_name: str = Field( + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Identifier of the agent making the evaluation request", ) step: Step = Field( ..., description="Agent step payload to evaluate" @@ -35,7 +37,7 @@ class EvaluationRequest(BaseModel): "json_schema_extra": { "examples": [ { - "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + "agent_name": "customer-service-bot", "step": { "type": "llm", "name": "support-answer", @@ -45,7 +47,7 @@ class EvaluationRequest(BaseModel): "stage": "pre" }, { - "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + "agent_name": "customer-service-bot", "step": { "type": "llm", "name": "support-answer", @@ -56,7 +58,7 @@ class EvaluationRequest(BaseModel): "stage": "post" }, { - "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + "agent_name": "customer-service-bot", "step": { "type": "tool", "name": "search_database", @@ -66,7 +68,7 @@ class EvaluationRequest(BaseModel): "stage": "pre" }, { - "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + "agent_name": "customer-service-bot", "step": { "type": "tool", "name": "search_database", @@ -80,6 +82,11 @@ class EvaluationRequest(BaseModel): } } + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) + class EvaluationResponse(BaseModel): """ diff --git a/models/src/agent_control_models/observability.py b/models/src/agent_control_models/observability.py index faa66087..ae01422c 100644 --- a/models/src/agent_control_models/observability.py +++ b/models/src/agent_control_models/observability.py @@ -10,10 +10,11 @@ from datetime import UTC, datetime from typing import Any, Literal -from uuid import UUID, uuid4 +from uuid import uuid4 from pydantic import Field, field_validator +from .agent import AGENT_NAME_MIN_LENGTH, AGENT_NAME_PATTERN, normalize_agent_name from .base import BaseModel # ============================================================================= @@ -36,8 +37,7 @@ class ControlExecutionEvent(BaseModel): control_execution_id: Unique ID for this specific control execution trace_id: OpenTelemetry-compatible trace ID (128-bit hex, 32 chars) span_id: OpenTelemetry-compatible span ID (64-bit hex, 16 chars) - agent_uuid: UUID of the agent that executed the control - agent_name: Name of the agent (denormalized for queries) + agent_name: Identifier of the agent that executed the control control_id: Database ID of the control control_name: Name of the control (denormalized for queries) check_stage: "pre" (before execution) or "post" (after execution) @@ -70,8 +70,12 @@ class ControlExecutionEvent(BaseModel): ) # Agent identity - agent_uuid: UUID = Field(..., description="UUID of the agent") - agent_name: str = Field(..., description="Name of the agent (denormalized)") + agent_name: str = Field( + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Identifier of the agent", + ) # Control info control_id: int = Field(..., description="Database ID of the control") @@ -151,6 +155,11 @@ def validate_span_id(cls, v: str) -> str: raise ValueError("span_id cannot be empty") return v + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) + model_config = { "json_schema_extra": { "examples": [ @@ -158,7 +167,6 @@ def validate_span_id(cls, v: str) -> str: "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", - "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", "agent_name": "my-agent", "control_id": 123, "control_name": "sql-injection-check", @@ -205,7 +213,6 @@ class BatchEventsRequest(BaseModel): { "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7", - "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", "agent_name": "my-agent", "control_id": 123, "control_name": "sql-injection-check", @@ -256,7 +263,7 @@ class EventQueryRequest(BaseModel): trace_id: Filter by trace ID (get all events for a request) span_id: Filter by span ID (get all events for a function call) control_execution_id: Filter by specific event ID - agent_uuid: Filter by agent UUID + agent_name: Filter by agent identifier control_ids: Filter by control IDs actions: Filter by actions (allow, deny, warn, log) matched: Filter by matched status @@ -277,7 +284,12 @@ class EventQueryRequest(BaseModel): control_execution_id: str | None = Field( default=None, description="Filter by specific event ID" ) - agent_uuid: UUID | None = Field(default=None, description="Filter by agent UUID") + agent_name: str | None = Field( + default=None, + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Filter by agent identifier", + ) control_ids: list[int] | None = Field( default=None, description="Filter by control IDs" ) @@ -305,7 +317,7 @@ class EventQueryRequest(BaseModel): "examples": [ {"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"}, { - "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + "agent_name": "my-agent", "actions": ["deny", "warn"], "start_time": "2025-01-09T00:00:00Z", "limit": 50, @@ -314,6 +326,15 @@ class EventQueryRequest(BaseModel): } } + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name( + cls, value: str | None + ) -> str | None: + if value is None: + return None + return normalize_agent_name(str(value)) + class EventQueryResponse(BaseModel): """ @@ -377,12 +398,17 @@ class StatsRequest(BaseModel): Request model for aggregated statistics. Attributes: - agent_uuid: Agent to get stats for + agent_name: Agent to get stats for time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) include_timeseries: Whether to include time-series data points """ - agent_uuid: UUID = Field(..., description="Agent UUID") + agent_name: str = Field( + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Agent identifier", + ) time_range: Literal["1m", "5m", "15m", "1h", "24h", "7d", "30d", "180d", "365d"] = Field( default="5m", description="Time range" ) @@ -390,6 +416,11 @@ class StatsRequest(BaseModel): default=False, description="Include time-series data points for trend visualization" ) + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) + class TimeseriesBucket(BaseModel): """ @@ -476,19 +507,29 @@ class StatsResponse(BaseModel): Contains agent-level totals (with optional timeseries) and per-control breakdown. Attributes: - agent_uuid: Agent UUID + agent_name: Agent identifier time_range: Time range used totals: Agent-level aggregate statistics (includes timeseries) controls: Per-control breakdown for discovery and detail """ - agent_uuid: UUID = Field(..., description="Agent UUID") + agent_name: str = Field( + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Agent identifier", + ) time_range: str = Field(..., description="Time range used") totals: StatsTotals = Field(..., description="Agent-level aggregate statistics") controls: list[ControlStats] = Field( ..., description="Per-control breakdown" ) + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) + class ControlStatsResponse(BaseModel): """ @@ -497,15 +538,25 @@ class ControlStatsResponse(BaseModel): Contains stats for a single control (with optional timeseries). Attributes: - agent_uuid: Agent UUID + agent_name: Agent identifier time_range: Time range used control_id: Control ID control_name: Control name stats: Control statistics (includes timeseries when requested) """ - agent_uuid: UUID = Field(..., description="Agent UUID") + agent_name: str = Field( + ..., + min_length=AGENT_NAME_MIN_LENGTH, + pattern=AGENT_NAME_PATTERN, + description="Agent identifier", + ) time_range: str = Field(..., description="Time range used") control_id: int = Field(..., description="Control ID") control_name: str = Field(..., description="Control name") stats: StatsTotals = Field(..., description="Control statistics") + + @field_validator("agent_name", mode="before") + @classmethod + def validate_and_normalize_agent_name(cls, value: str) -> str: + return normalize_agent_name(str(value)) diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index cdd8d03e..693c439a 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -141,7 +141,6 @@ class InitAgentRequest(BaseModel): "examples": [ { "agent": { - "agent_id": "550e8400-e29b-41d4-a716-446655440000", "agent_name": "customer-service-bot", "agent_description": "Handles customer inquiries", "agent_version": "1.0.0", @@ -310,8 +309,7 @@ class PatchAgentResponse(BaseModel): class AgentSummary(BaseModel): """Summary of an agent for list responses.""" - agent_id: str = Field(..., description="UUID of the agent") - agent_name: str = Field(..., description="Human-readable name of the agent") + agent_name: str = Field(..., description="Unique identifier of the agent") policy_id: int | None = Field(None, description="ID of assigned policy, if any") created_at: str | None = Field(None, description="ISO 8601 timestamp when agent was created") step_count: int = Field(0, description="Number of steps registered with the agent") @@ -347,8 +345,7 @@ class ListAgentsResponse(BaseModel): class AgentRef(BaseModel): """Reference to an agent (for listing which agents use a control).""" - agent_id: str = Field(..., description="Agent UUID") - agent_name: str = Field(..., description="Agent name") + agent_name: str = Field(..., description="Agent identifier") class ControlSummary(BaseModel): diff --git a/sdks/python/ARCHITECTURE.md b/sdks/python/ARCHITECTURE.md index 86625b10..6c1d8db3 100644 --- a/sdks/python/ARCHITECTURE.md +++ b/sdks/python/ARCHITECTURE.md @@ -49,11 +49,11 @@ async with AgentControlClient(base_url="http://localhost:8000") as client: **Endpoints Covered**: - `POST /api/v1/agents/initAgent` - Register or update an agent -- `GET /api/v1/agents/{agent_id}` - Get agent details +- `GET /api/v1/agents/{agent_name}` - Get agent details **Functions**: - `async def register_agent(client, agent, tools)` - Register an agent with tools -- `async def get_agent(client, agent_id)` - Fetch agent details by ID +- `async def get_agent(client, agent_name)` - Fetch agent details by ID **Usage**: ```python @@ -219,8 +219,8 @@ class MyCustomEvaluator(Evaluator): **Purpose**: Public API, convenience functions, and initialization. **Key Functions**: -- `init(agent_name, agent_id, ...)` - Initialize Agent Control -- `get_agent(agent_id, server_url)` - Convenience function for fetching agents +- `init(agent_name, agent_name, ...)` - Initialize Agent Control +- `get_agent(agent_name, server_url)` - Convenience function for fetching agents - `list_agents()` - List all registered agents - `current_agent()` - Get the currently initialized agent - `control()` - Decorator for server-side policy enforcement diff --git a/sdks/python/README.md b/sdks/python/README.md index 8607c60b..8b607e2a 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="550e8400-e29b-41d4-a716-446655440000" + agent_name="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="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries and support", agent_version="2.1.0", server_url="http://localhost:8000", @@ -55,7 +55,7 @@ One line to set up your agent with full protection: ```python agent_control.init( agent_name="...", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", ) ``` @@ -90,7 +90,7 @@ async with AgentControlClient() as client: # Evaluate a step result = await agent_control.evaluation.check_evaluation( client, - agent_uuid="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", step={"type": "llm_inference", "input": "User input here"}, stage="pre" ) @@ -103,7 +103,7 @@ Access your agent information: ```python agent = agent_control.current_agent() print(f"Agent: {agent.agent_name}") -print(f"ID: {agent.agent_id}") +print(f"ID: {agent.agent_name}") print(f"Version: {agent.agent_version}") ``` @@ -117,7 +117,7 @@ from agent_control import control, ControlViolationError # Initialize agent_control.init( agent_name="Customer Support Bot", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", agent_version="1.0.0" ) @@ -158,7 +158,7 @@ asyncio.run(main()) ```python def init( agent_name: str, - agent_id: str | UUID, + agent_name: str | UUID, agent_description: Optional[str] = None, agent_version: Optional[str] = None, server_url: Optional[str] = None, @@ -171,7 +171,7 @@ Initialize Agent Control with your agent's information. **Parameters:** - `agent_name`: Human-readable name -- `agent_id`: UUID string (or UUID instance) +- `agent_name`: 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) @@ -278,7 +278,7 @@ from agent_control import control, ControlViolationError agent_control.init( agent_name="...", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", ) @control() diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index c7078f5e..b48db32f 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -9,8 +9,7 @@ # Initialize at the base of your agent file agent_control.init( - agent_name="my-customer-service-bot", - agent_id="550e8400-e29b-41d4-a716-446655440000" + agent_name="my-customer-service-bot" ) # Apply server-defined controls using the decorator @@ -33,7 +32,7 @@ async def handle_input(user_message: str) -> str: async with agent_control.AgentControlClient() as client: result = await agent_control.evaluation.check_evaluation( client, - agent_uuid, + agent_name, step={"type": "llm", "name": "chat", "input": "Hello"}, stage="pre", ) @@ -50,7 +49,6 @@ async def handle_input(user_message: str) -> str: from collections.abc import Callable from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Literal, TypeVar -from uuid import UUID import httpx @@ -107,7 +105,7 @@ async def handle_input(user_message: str) -> str: is_otel_available, with_trace, ) -from .validation import ensure_uuid +from .validation import ensure_agent_name # Module logger logger = get_logger(__name__) @@ -156,11 +154,9 @@ class EvaluatorSpec: class Agent: # runtime fallback def __init__( self, - agent_id: str | UUID, agent_name: str, **kwargs: object ): - self.agent_id = agent_id self.agent_name = agent_name for k, v in kwargs.items(): setattr(self, k, v) @@ -200,11 +196,11 @@ def __init__( class EvaluationRequest: # runtime fallback def __init__( self, - agent_uuid: UUID, + agent_name: str, step: Step, stage: str, ): - self.agent_uuid = agent_uuid + self.agent_name = agent_name self.step = step self.stage = stage @@ -350,7 +346,6 @@ def run_in_thread() -> None: def init( agent_name: str, - agent_id: str | UUID, agent_description: str | None = None, agent_version: str | None = None, server_url: str | None = None, @@ -374,8 +369,7 @@ def init( 5. Enable the @control decorator Args: - agent_name: Human-readable name for your agent (e.g., "Customer Service Bot") - agent_id: Unique identifier for your agent (UUID string or UUID instance) + agent_name: Unique identifier for your agent (will be normalized to lowercase) 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 @@ -398,8 +392,7 @@ def init( import agent_control agent_control.init( - agent_name="Customer Service Bot", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="customer-service-bot", agent_description="Handles customer inquiries and support tickets", agent_version="2.1.0", steps=[ @@ -424,16 +417,15 @@ async def handle(message: str): """ global _current_agent, _control_engine, _client, _server_controls, _server_url, _api_key - if not agent_id: + if not agent_name: raise ValueError( - "The 'agent_id' argument is required for initialization.\n" - "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")' + "The 'agent_name' argument is required for initialization.\n" + "Please provide a valid agent identifier, e.g.:\n" + ' agent_control.init(agent_name="customer-service-bot")' ) - # Validate agent_id is a UUID (string or UUID instance) - _agent_uuid = ensure_uuid(agent_id) + # Validate and normalize agent_name + _agent_name = ensure_agent_name(agent_name) # Configure logging if provided (do this early before any logging happens) if log_config: @@ -441,8 +433,7 @@ async def handle(message: str): # Create agent instance with metadata _current_agent = Agent( - agent_id=_agent_uuid, - agent_name=agent_name, + agent_name=_agent_name, agent_description=agent_description, agent_created_at=datetime.now(UTC).isoformat(), agent_updated_at=None, @@ -505,16 +496,16 @@ async def register() -> list[dict[str, Any]] | None: controls: list[dict[str, Any]] = response.get('controls', []) if created: - logger.info("Agent registered: %s (ID: %s)", agent_name, _agent_uuid) + logger.info("Agent registered: %s", _agent_name) else: - logger.info("Agent updated: %s (ID: %s)", agent_name, _agent_uuid) + logger.info("Agent updated: %s", _agent_name) if registration_steps: logger.debug("Registered %d step(s)", len(registration_steps)) return controls except httpx.HTTPStatusError: - # Surface API errors like UUID conflicts + # Surface API errors like name conflicts raise except Exception as e: logger.error("Failed to register agent: %s", e, exc_info=True) @@ -556,7 +547,7 @@ def run_in_thread() -> None: loop.close() except httpx.HTTPStatusError: - # Surface server-side errors (e.g., 409 UUID conflicts) + # Surface server-side errors (e.g., 409 conflicts) raise except Exception as e: logger.error("Could not connect to server: %s", e, exc_info=True) @@ -585,21 +576,21 @@ def run_in_thread() -> None: async def get_agent( - agent_id: str | UUID, + agent_name: str, server_url: str | None = None, api_key: str | None = None, ) -> dict[str, Any]: """ - Get agent details from the server by ID. + Get agent details from the server by name. Args: - agent_id: UUID string or UUID instance + agent_name: Agent identifier 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) Returns: Dictionary containing: - - agent: Agent metadata (agent_name, agent_id, etc.) + - agent: Agent metadata - steps: List of steps registered with the agent Raises: @@ -612,7 +603,7 @@ async def get_agent( # Fetch agent from server async def main(): agent_data = await agent_control.get_agent( - "550e8400-e29b-41d4-a716-446655440000" + "customer-service-bot" ) print(f"Agent: {agent_data['agent']['agent_name']}") print(f"Steps: {len(agent_data['steps'])}") @@ -622,13 +613,13 @@ async def main(): # Or using the client directly async with agent_control.AgentControlClient() as client: agent_data = await agent_control.agents.get_agent( - client, "550e8400-e29b-41d4-a716-446655440000" + client, "customer-service-bot" ) """ _final_server_url = server_url or os.getenv('AGENT_CONTROL_URL') or 'http://localhost:8000' async with AgentControlClient(base_url=_final_server_url, api_key=api_key) as client: - return await agents.get_agent(client, agent_id) + return await agents.get_agent(client, agent_name) def current_agent() -> Agent | None: @@ -641,7 +632,6 @@ def current_agent() -> Agent | None: Example: 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" @@ -661,12 +651,12 @@ async def list_agents( Args: 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) - cursor: Optional cursor for pagination (UUID of last agent from previous page) + cursor: Optional cursor for pagination (agent name of last item from previous page) limit: Number of results per page (default 20, max 100) Returns: Dictionary containing: - - agents: List of agent summaries with agent_id, agent_name, + - agents: List of agent summaries with agent_name, policy_id, created_at, step_count, evaluator_count - pagination: Object with limit, total, next_cursor, has_more @@ -681,7 +671,7 @@ async def main(): result = await agent_control.list_agents() print(f"Total agents: {result['pagination']['total']}") for agent in result['agents']: - print(f" - {agent['agent_name']} ({agent['agent_id']})") + print(f" - {agent['agent_name']}") # Fetch next page if result['pagination']['has_more']: next_page = await agent_control.list_agents( diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index 2706f4bd..1a390be0 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -1,16 +1,16 @@ """Agent management operations for Agent Control SDK.""" from typing import Any, Literal, cast -from uuid import UUID from agent_control_engine import ensure_evaluators_discovered from .client import AgentControlClient -from .validation import ensure_uuid_str +from .validation import ensure_agent_name # Import models if available try: from agent_control_models import Agent + MODELS_AVAILABLE = True except ImportError: MODELS_AVAILABLE = False @@ -23,28 +23,7 @@ async def register_agent( steps: list[dict[str, Any]] | None = None, conflict_mode: Literal["strict", "overwrite"] = "overwrite", ) -> dict[str, Any]: - """ - Register an agent with the server via /initAgent endpoint. - - Args: - client: AgentControlClient instance - agent: Agent instance to register - steps: Optional list of step schemas - conflict_mode: How to handle step/evaluator conflicts during initAgent. - Defaults to "overwrite" for SDK registration flows. - - Returns: - InitAgentResponse with created flag and controls - - Raises: - httpx.HTTPError: If request fails - - Example: - async with AgentControlClient() as client: - response = await register_agent(client, agent, steps=[...]) - print(f"Created: {response['created']}") - """ - # Ensure evaluators are discovered for local evaluation support + """Register an agent with the server via /initAgent endpoint.""" ensure_evaluators_discovered() if steps is None: @@ -52,9 +31,7 @@ async def register_agent( if MODELS_AVAILABLE: agent_dict = agent.to_dict() - # Ensure UUID is converted to string for JSON serialization - if isinstance(agent_dict.get('agent_id'), UUID): - agent_dict['agent_id'] = str(agent_dict['agent_id']) + agent_dict["agent_name"] = ensure_agent_name(str(agent_dict.get("agent_name", ""))) payload = { "agent": agent_dict, "steps": steps, @@ -63,11 +40,10 @@ async def register_agent( else: payload = { "agent": { - "agent_id": str(agent.agent_id), - "agent_name": agent.agent_name, - "agent_description": getattr(agent, 'agent_description', None), - "agent_version": getattr(agent, 'agent_version', None), - "agent_metadata": getattr(agent, 'agent_metadata', None), + "agent_name": ensure_agent_name(str(agent.agent_name)), + "agent_description": getattr(agent, "agent_description", None), + "agent_version": getattr(agent, "agent_version", None), + "agent_metadata": getattr(agent, "agent_metadata", None), }, "steps": steps, "conflict_mode": conflict_mode, @@ -78,33 +54,10 @@ async def register_agent( return cast(dict[str, Any], response.json()) -async def get_agent( - client: AgentControlClient, - agent_id: str | UUID -) -> dict[str, Any]: - """ - Get agent details by ID from the server. - - Args: - client: AgentControlClient instance - agent_id: UUID string or UUID instance - - Returns: - Dictionary containing: - - agent: Agent metadata - - steps: List of steps registered with the agent - - Raises: - httpx.HTTPError: If request fails or agent not found (404) - - Example: - async with AgentControlClient() as client: - agent_data = await get_agent(client, "550e8400-e29b-41d4-a716-446655440000") - print(f"Agent: {agent_data['agent']['agent_name']}") - print(f"Steps: {len(agent_data['steps'])}") - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}") +async def get_agent(client: AgentControlClient, agent_name: str) -> dict[str, Any]: + """Get agent details by name from the server.""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.get(f"/api/v1/agents/{normalized_name}") response.raise_for_status() return cast(dict[str, Any], response.json()) @@ -114,38 +67,10 @@ async def list_agents( cursor: str | None = None, limit: int = 20, ) -> dict[str, Any]: - """ - List all registered agents from the server. - - Args: - client: AgentControlClient instance - cursor: Optional cursor for pagination (UUID of last agent from previous page) - limit: Number of results per page (default 20, max 100) - - Returns: - Dictionary containing: - - agents: List of agent summaries with agent_id, agent_name, - policy_id, created_at, step_count, evaluator_count - - pagination: Object with limit, total, next_cursor, has_more - - Raises: - httpx.HTTPError: If request fails - - Example: - async with AgentControlClient() as client: - result = await list_agents(client, limit=10) - print(f"Total agents: {result['pagination']['total']}") - for agent in result['agents']: - print(f" - {agent['agent_name']} ({agent['agent_id']})") - # Fetch next page if available - if result['pagination']['has_more']: - next_result = await list_agents( - client, cursor=result['pagination']['next_cursor'] - ) - """ + """List all registered agents from the server.""" params: dict[str, Any] = {"limit": limit} if cursor: - params["cursor"] = cursor + params["cursor"] = ensure_agent_name(cursor) response = await client.http_client.get("/api/v1/agents", params=params) response.raise_for_status() return cast(dict[str, Any], response.json()) @@ -153,51 +78,21 @@ async def list_agents( async def get_agent_policy( client: AgentControlClient, - agent_id: str | UUID, + agent_name: str, ) -> dict[str, Any]: - """ - Get the policy assigned to an agent. - - Args: - client: AgentControlClient instance - agent_id: UUID string or UUID instance - - Returns: - Dictionary containing: - - policy_id: ID of the policy assigned to the agent - - Raises: - httpx.HTTPError: If request fails or agent has no policy - - Example: - async with AgentControlClient() as client: - policy = await get_agent_policy(client, agent_id) - print(f"Policy ID: {policy['policy_id']}") - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/policy") + """Get the policy assigned to an agent.""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.get(f"/api/v1/agents/{normalized_name}/policy") response.raise_for_status() return cast(dict[str, Any], response.json()) async def remove_agent_policy( client: AgentControlClient, - agent_id: str | UUID, + agent_name: str, ) -> dict[str, Any]: - """ - Remove the policy assignment from an agent. - - Args: - client: AgentControlClient instance - agent_id: UUID string or UUID instance - - Returns: - Dictionary containing success flag/details - - Raises: - httpx.HTTPError: If request fails or agent has no policy - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.delete(f"/api/v1/agents/{agent_id_str}/policy") + """Remove the policy assignment from an agent.""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.delete(f"/api/v1/agents/{normalized_name}/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 fdcfcf37..e671e4fb 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -12,8 +12,7 @@ import agent_control agent_control.init( - agent_name="my-agent", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="my-agent-identity", ) # Apply the agent's assigned policy @@ -60,7 +59,6 @@ class ControlContext: control wrappers, including stats tracking, result processing, and logging. """ - agent_uuid: str agent_name: str server_url: str func: Callable @@ -188,14 +186,14 @@ def _get_server_url() -> str: async def _evaluate( - agent_uuid: str, + agent_name: str, step: dict[str, Any], stage: str, server_url: str, trace_id: str | None = None, span_id: str | None = None, controls: list[dict[str, Any]] | None = None, - agent_name: str | None = None, + event_agent_name: str | None = None, ) -> dict[str, Any]: """Call evaluation with support for local (SDK) and server execution. @@ -216,8 +214,6 @@ async def _evaluate( # If we have controls, use local evaluation which handles both SDK and server controls if controls is not None: try: - from uuid import UUID - from agent_control.evaluation import check_evaluation_with_local # Build Step object for evaluation @@ -229,13 +225,13 @@ async def _evaluate( result = await check_evaluation_with_local( client=client, - agent_uuid=UUID(agent_uuid), + agent_name=agent_name, step=step_obj, stage=stage, # type: ignore controls=controls, trace_id=trace_id, span_id=span_id, - agent_name=agent_name, + event_agent_name=event_agent_name, ) # Convert result to dict format expected by process_result @@ -308,7 +304,7 @@ async def _evaluate( response = await client.http_client.post( "/api/v1/evaluation", json={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "step": step, "stage": stage }, @@ -577,7 +573,6 @@ async def _execute_with_control( trace_id, span_id = get_trace_and_span_ids() # New trace and span ctx = ControlContext( - agent_uuid=str(agent.agent_id), agent_name=agent.agent_name, server_url=_get_server_url(), func=func, @@ -594,10 +589,10 @@ async def _execute_with_control( # PRE-EXECUTION: Check controls with check_stage="pre" try: result = await _evaluate( - ctx.agent_uuid, ctx.pre_payload(), "pre", + ctx.agent_name, ctx.pre_payload(), "pre", ctx.server_url, ctx.trace_id, ctx.span_id, controls=controls, - agent_name=ctx.agent_name, + event_agent_name=ctx.agent_name, ) ctx.process_result(result, "pre") except ControlViolationError: @@ -618,10 +613,10 @@ async def _execute_with_control( # POST-EXECUTION: Check controls with check_stage="post" try: result = await _evaluate( - ctx.agent_uuid, ctx.post_payload(output), "post", + ctx.agent_name, ctx.post_payload(output), "post", ctx.server_url, ctx.trace_id, ctx.span_id, controls=controls, - agent_name=ctx.agent_name, + event_agent_name=ctx.agent_name, ) ctx.process_result(result, "post") except ControlViolationError: @@ -666,8 +661,7 @@ def control(policy: str | None = None, step_name: str | None = None) -> Callable # Initialize agent (connects to server, loads policy) agent_control.init( - agent_name="my-bot", - agent_id="550e8400-e29b-41d4-a716-446655440000", + agent_name="my-bot-identity", ) # Apply the agent's policy (all controls) @@ -695,7 +689,7 @@ async def handle_user_input(user_message: str) -> str: POST /api/v1/policies/{policy_id}/controls/{control_id} 3. Assign policy to agent: - POST /api/v1/agents/{agent_id}/policy/{policy_id} + POST /api/v1/agents/{agent_name}/policy/{policy_id} """ # The policy parameter is for documentation only - the server uses # the agent's assigned policy automatically diff --git a/sdks/python/src/agent_control/evaluation.py b/sdks/python/src/agent_control/evaluation.py index deddd6ee..81d45cff 100644 --- a/sdks/python/src/agent_control/evaluation.py +++ b/sdks/python/src/agent_control/evaluation.py @@ -3,16 +3,15 @@ from dataclasses import dataclass from datetime import UTC, datetime from typing import Any, Literal, cast -from uuid import UUID from .client import AgentControlClient from .observability import add_event, get_logger, is_observability_enabled +from .validation import ensure_agent_name _logger = get_logger(__name__) # Fallback IDs used when trace context is missing. -# All-zero values are invalid trace/span IDs per OpenTelemetry, making them -# easy to filter in observability queries while still recording the event. +# All-zero values are invalid trace/span IDs per OpenTelemetry. _FALLBACK_TRACE_ID = "0" * 32 _FALLBACK_SPAN_ID = "0" * 16 _trace_warning_logged = False @@ -37,7 +36,6 @@ except ImportError: MODELS_AVAILABLE = False ENGINE_AVAILABLE = False - # Runtime fallbacks Step = Any # type: ignore EvaluationRequest = Any # type: ignore EvaluationResponse = Any # type: ignore @@ -50,10 +48,6 @@ def _map_applies_to(step_type: str) -> Literal["llm_call", "tool_call"]: - """Map step type to observability applies_to value. - - Matches the server pattern at endpoints/evaluation.py. - """ return "tool_call" if step_type == "tool" else "llm_call" @@ -65,19 +59,8 @@ def _emit_local_events( span_id: str | None, agent_name: str | None, ) -> None: - """Emit observability events for locally-evaluated controls. - - Mirrors the server's _emit_observability_events() so that SDK-evaluated - controls are visible in the observability pipeline. - - When trace_id/span_id are missing, fallback all-zero IDs are used so events - are still recorded (but clearly marked as uncorrelated). - - Only runs when observability is enabled. - """ - if not is_observability_enabled(): - return - if not ENGINE_AVAILABLE: + """Emit observability events for locally-evaluated controls.""" + if not is_observability_enabled() or not ENGINE_AVAILABLE: return global _trace_warning_logged # noqa: PLW0603 @@ -95,31 +78,31 @@ def _emit_local_events( applies_to = _map_applies_to(request.step.type) control_lookup = {c.id: c for c in local_controls} now = datetime.now(UTC) + resolved_agent_name = agent_name or request.agent_name def _emit_matches(matches: list[ControlMatch] | None, matched: bool) -> None: if not matches: return - for m in matches: - ctrl = control_lookup.get(m.control_id) + for match in matches: + ctrl = control_lookup.get(match.control_id) add_event( ControlExecutionEvent( - control_execution_id=m.control_execution_id, + control_execution_id=match.control_execution_id, trace_id=trace_id, span_id=span_id, - agent_uuid=request.agent_uuid, - agent_name=agent_name or "unknown", - control_id=m.control_id, - control_name=m.control_name, + agent_name=resolved_agent_name, + control_id=match.control_id, + control_name=match.control_name, check_stage=request.stage, applies_to=applies_to, - action=m.action, + action=match.action, matched=matched, - confidence=m.result.confidence, + confidence=match.result.confidence, timestamp=now, evaluator_name=ctrl.control.evaluator.name if ctrl else None, selector_path=ctrl.control.selector.path if ctrl else None, - error_message=m.result.error if not matched else None, - metadata=m.result.metadata or {}, + error_message=match.result.error if not matched else None, + metadata=match.result.metadata or {}, ) ) @@ -130,58 +113,21 @@ def _emit_matches(matches: list[ControlMatch] | None, matched: bool) -> None: async def check_evaluation( client: AgentControlClient, - agent_uuid: UUID, + agent_name: str, step: "Step", stage: Literal["pre", "post"], ) -> EvaluationResult: - """ - Check if agent interaction is safe. - - Args: - client: AgentControlClient instance - agent_uuid: UUID of the agent making the request - step: Step payload to evaluate - stage: 'pre' for pre-execution check, 'post' for post-execution check - - Returns: - EvaluationResult with safety analysis - - Raises: - httpx.HTTPError: If request fails - - Example: - # Pre-check before LLM step - async with AgentControlClient() as client: - result = await check_evaluation( - client=client, - agent_uuid=agent.agent_id, - step={"type": "llm", "name": "support-answer", "input": "User question"}, - stage="pre" - ) + """Check if agent interaction is safe.""" + normalized_name = ensure_agent_name(agent_name) - # Post-check after tool execution - async with AgentControlClient() as client: - result = await check_evaluation( - client=client, - agent_uuid=agent.agent_id, - step={ - "type": "tool", - "name": "search", - "input": {"query": "test"}, - "output": {"results": []}, - }, - stage="post" - ) - """ if MODELS_AVAILABLE: request = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=normalized_name, step=step, stage=stage, ) request_payload = request.model_dump(mode="json") else: - # Fallback for when models aren't available if isinstance(step, dict): step_dict = step else: @@ -198,7 +144,7 @@ async def check_evaluation( raise ValueError("step.name is required for evaluation requests") request_payload = { - "agent_uuid": str(agent_uuid), + "agent_name": normalized_name, "step": step_dict, "stage": stage, } @@ -208,15 +154,16 @@ async def check_evaluation( if MODELS_AVAILABLE: return cast(EvaluationResult, EvaluationResult.from_dict(response.json())) - else: - data = response.json() - # Create a simple result object - class _EvaluationResult: - def __init__(self, is_safe: bool, confidence: float, reason: str | None = None): - self.is_safe = is_safe - self.confidence = confidence - self.reason = reason - return cast(EvaluationResult, _EvaluationResult(**data)) + + data = response.json() + + class _EvaluationResult: + def __init__(self, is_safe: bool, confidence: float, reason: str | None = None): + self.is_safe = is_safe + self.confidence = confidence + self.reason = reason + + return cast(EvaluationResult, _EvaluationResult(**data)) @dataclass @@ -232,35 +179,22 @@ def _merge_results( local_result: "EvaluationResponse", server_result: "EvaluationResponse", ) -> "EvaluationResult": - """Merge local and server evaluation results. - - Merge semantics: - - is_safe: False if either is False (deny from either → deny) - - confidence: min of both (most conservative) - - matches: combined from both - - errors: combined from both - """ + """Merge local and server evaluation results.""" is_safe = local_result.is_safe and server_result.is_safe - - # Use minimum confidence (most conservative) confidence = min(local_result.confidence, server_result.confidence) - # Combine matches matches: list[ControlMatch] | None = None if local_result.matches or server_result.matches: matches = (local_result.matches or []) + (server_result.matches or []) - # Combine errors errors: list[ControlMatch] | None = None if local_result.errors or server_result.errors: errors = (local_result.errors or []) + (server_result.errors or []) - # Combine non_matches non_matches: list[ControlMatch] | None = None if local_result.non_matches or server_result.non_matches: non_matches = (local_result.non_matches or []) + (server_result.non_matches or []) - # Combine reasons reason = None if local_result.reason and server_result.reason: reason = f"{local_result.reason}; {server_result.reason}" @@ -281,113 +215,70 @@ def _merge_results( async def check_evaluation_with_local( client: AgentControlClient, - agent_uuid: UUID, + agent_name: str, step: "Step", stage: Literal["pre", "post"], controls: list[dict[str, Any]], trace_id: str | None = None, span_id: str | None = None, - agent_name: str | None = None, + event_agent_name: str | None = None, ) -> EvaluationResult: - """ - Check if agent interaction is safe, running local controls first. - - This function executes controls with execution="sdk" locally in the SDK, - then calls the server for execution="server" controls. If a local control - denies, it short-circuits and returns immediately without calling the server. - - Note on parse errors: If a local control fails to parse/validate, it is - skipped (logged as WARNING) and the error is included in result.errors. - This does NOT affect is_safe or confidence—callers concerned with safety - should check result.errors for any parse failures. - - Args: - client: AgentControlClient instance - agent_uuid: UUID of the agent making the request - step: Step payload to evaluate - stage: 'pre' for pre-execution check, 'post' for post-execution check - controls: List of control dicts from initAgent response - (each has 'id', 'name', 'control' keys) - - Returns: - EvaluationResult with safety analysis (merged from local + server) - - Raises: - httpx.HTTPError: If server request fails - RuntimeError: If engine is not available - - Example: - # Get controls from initAgent - init_response = await register_agent(client, agent, steps) - controls = init_response.get('controls', []) - - # Check with local execution - result = await check_evaluation_with_local( - client=client, - agent_uuid=agent.agent_id, - step={"type": "llm", "name": "support-answer", "input": "User question"}, - stage="pre", - controls=controls, - ) - """ + """Check safety, running local SDK controls before server controls.""" if not ENGINE_AVAILABLE: raise RuntimeError( "Local evaluation requires agent_control_engine. " "Install with: pip install agent-control-engine" ) - # Partition controls by local flag + normalized_name = ensure_agent_name(agent_name) + local_controls: list[_ControlAdapter] = [] parse_errors: list[ControlMatch] = [] has_server_controls = False - for c in controls: - control_data = c.get("control", {}) + for control in controls: + control_data = control.get("control", {}) execution = control_data.get("execution", "server") is_local = execution == "sdk" - # Track server controls early, before any parsing that might fail if not is_local: has_server_controls = True - continue # Server controls are handled by the server, not parsed here + continue - # Parse and validate local controls try: control_def = ControlDefinition.model_validate(control_data) - - # Validate evaluator is available locally evaluator_name = control_def.evaluator.name - # Agent-scoped evaluators (agent:evaluator) are server-only + if ":" in evaluator_name: raise RuntimeError( - f"Control '{c['name']}' is marked execution='sdk' but uses " + f"Control '{control['name']}' is marked execution='sdk' but uses " f"agent-scoped evaluator '{evaluator_name}' which is server-only. " "Set execution='server' or use a built-in evaluator." ) if evaluator_name not in list_evaluators(): raise RuntimeError( - f"Control '{c['name']}' is marked execution='sdk' but evaluator " + f"Control '{control['name']}' is marked execution='sdk' but evaluator " f"'{evaluator_name}' is not available in the SDK. " "Install the evaluator or set execution='server'." ) - local_controls.append(_ControlAdapter( - id=c["id"], - name=c["name"], - control=control_def, - )) + local_controls.append( + _ControlAdapter( + id=control["id"], + name=control["name"], + control=control_def, + ) + ) except RuntimeError: - # Re-raise our explicit errors raise - except Exception as e: - # Validation/parse error - log and add to errors list - control_id = c.get("id", -1) - control_name = c.get("name", "unknown") + except Exception as exc: + control_id = control.get("id", -1) + control_name = control.get("name", "unknown") _logger.warning( "Skipping invalid local control '%s' (id=%s): %s", control_name, control_id, - e, + exc, ) parse_errors.append( ControlMatch( @@ -397,13 +288,12 @@ async def check_evaluation_with_local( result=EvaluatorResult( matched=False, confidence=0.0, - error=f"Failed to parse local control: {e}", + error=f"Failed to parse local control: {exc}", ), ) ) def _with_parse_errors(result: EvaluationResult) -> EvaluationResult: - """Merge parse_errors into result.errors.""" if not parse_errors: return result combined_errors = (result.errors or []) + parse_errors @@ -416,27 +306,26 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult: non_matches=result.non_matches, ) - # Build evaluation request request = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=normalized_name, step=step, stage=stage, ) - # Run local controls if any local_result: EvaluationResponse | None = None if local_controls: engine = ControlEngine(local_controls, context="sdk") local_result = await engine.process(request) - # Emit observability events for locally-evaluated controls - # (before short-circuit so events are always emitted for local controls) _emit_local_events( - local_result, request, local_controls, - trace_id, span_id, agent_name, + local_result, + request, + local_controls, + trace_id, + span_id, + agent_name=event_agent_name, ) - # Short-circuit on local deny if not local_result.is_safe: return _with_parse_errors( EvaluationResult( @@ -449,22 +338,22 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult: ) ) - # Call server for non-local controls (if any exist) if has_server_controls: request_payload = request.model_dump(mode="json", exclude_none=True) - # Forward trace context as headers so server-emitted events have correct IDs headers: dict[str, str] = {} if trace_id: headers["X-Trace-Id"] = trace_id if span_id: headers["X-Span-Id"] = span_id + response = await client.http_client.post( - "/api/v1/evaluation", json=request_payload, headers=headers, + "/api/v1/evaluation", + json=request_payload, + headers=headers, ) response.raise_for_status() server_result = EvaluationResponse.model_validate(response.json()) - # Merge results if we had local controls if local_result is not None: return _with_parse_errors(_merge_results(local_result, server_result)) @@ -479,7 +368,6 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult: ) ) - # Only local controls existed (and they all passed) if local_result is not None: return _with_parse_errors( EvaluationResult( @@ -492,5 +380,4 @@ def _with_parse_errors(result: EvaluationResult) -> EvaluationResult: ) ) - # No controls at all - still include parse_errors if any return _with_parse_errors(EvaluationResult(is_safe=True, confidence=1.0)) diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index dab7ecaa..aef75bbf 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -1,10 +1,9 @@ """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 +from .validation import ensure_agent_name async def create_policy( @@ -152,7 +151,7 @@ async def list_policy_controls( async def assign_policy_to_agent( client: AgentControlClient, - agent_id: str | UUID, + agent_name: str, policy_id: int ) -> dict[str, Any]: """ @@ -162,7 +161,7 @@ async def assign_policy_to_agent( Args: client: AgentControlClient instance - agent_id: UUID string or UUID instance + agent_name: Agent identifier policy_id: ID of the policy to assign Returns: @@ -172,9 +171,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) + agent_name_str = ensure_agent_name(agent_name) response = await client.http_client.post( - f"/api/v1/agents/{agent_id_str}/policy/{policy_id}" + f"/api/v1/agents/{agent_name_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 index aaf95ff3..874b0072 100644 --- a/sdks/python/src/agent_control/validation.py +++ b/sdks/python/src/agent_control/validation.py @@ -2,19 +2,21 @@ from __future__ import annotations -from uuid import UUID +import re +_AGENT_NAME_MIN_LENGTH = 10 +_AGENT_NAME_REGEX = re.compile(r"^[a-z0-9:_-]+$") -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)) +def ensure_agent_name(value: str, field_name: str = "agent_name") -> str: + """Return normalized agent name or raise ValueError for invalid values.""" + normalized = str(value).strip().lower() + if len(normalized) < _AGENT_NAME_MIN_LENGTH: + raise ValueError( + f"{field_name} must be at least {_AGENT_NAME_MIN_LENGTH} characters long" + ) + if not _AGENT_NAME_REGEX.fullmatch(normalized): + raise ValueError( + f"{field_name} may only contain lowercase letters, digits, ':', '_' or '-'" + ) + return normalized diff --git a/sdks/python/tests/README.md b/sdks/python/tests/README.md index 71e690d7..b4677039 100644 --- a/sdks/python/tests/README.md +++ b/sdks/python/tests/README.md @@ -317,10 +317,10 @@ async def test_my_new_workflow( - Feature Y returns expected data """ # Arrange - agent_id = test_agent["agent_id"] + agent_name = test_agent["agent_name"] # Act - result = await agent_control.my_module.my_operation(client, agent_id) + result = await agent_control.my_module.my_operation(client, agent_name) # Assert assert result["success"] is True diff --git a/sdks/python/tests/conftest.py b/sdks/python/tests/conftest.py index b1201214..6860c8e9 100644 --- a/sdks/python/tests/conftest.py +++ b/sdks/python/tests/conftest.py @@ -101,8 +101,8 @@ def unique_name() -> str: @pytest.fixture def test_agent_id() -> str: - """Generate a unique agent ID for testing.""" - return str(uuid.uuid4()) + """Generate a unique agent name for testing.""" + return f"agent-{uuid.uuid4().hex[:12]}" @pytest_asyncio.fixture @@ -121,12 +121,8 @@ async def test_agent( from agent_control_models import Agent - # Generate a proper UUID4 for the agent - agent_uuid = uuid.uuid4() - agent = Agent( - agent_id=agent_uuid, - agent_name=f"Test Agent {test_agent_id}", + agent_name=test_agent_id, agent_description="Integration test agent", agent_created_at=datetime.now(UTC).isoformat(), agent_updated_at=None, @@ -150,7 +146,8 @@ async def test_agent( yield { "agent": agent, - "agent_id": str(agent_uuid), + "agent_name": test_agent_id, + "agent_name": test_agent_id, "response": response } diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py index 488538ac..20277e22 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -1,8 +1,6 @@ -"""SDK agent_id validation behavior tests.""" +"""SDK agent name validation behavior tests.""" from unittest.mock import AsyncMock, MagicMock -from uuid import uuid4 - import pytest from agent_control import agents, policies @@ -17,60 +15,96 @@ def json(self) -> dict: @pytest.mark.asyncio -async def test_get_agent_rejects_invalid_uuid() -> None: +async def test_get_agent_rejects_invalid_agent_name() -> 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") + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.get_agent(client, "short") client.http_client.get.assert_not_called() @pytest.mark.asyncio -async def test_get_agent_policy_rejects_invalid_uuid() -> None: +async def test_get_agent_policy_rejects_invalid_agent_name() -> 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") + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.get_agent_policy(client, "short") client.http_client.get.assert_not_called() @pytest.mark.asyncio -async def test_remove_agent_policy_rejects_invalid_uuid() -> None: +async def test_remove_agent_policy_rejects_invalid_agent_name() -> 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") + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.remove_agent_policy(client, "short") client.http_client.delete.assert_not_called() @pytest.mark.asyncio -async def test_assign_policy_rejects_invalid_uuid() -> None: +async def test_list_agents_normalizes_cursor() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) + + await agents.list_agents(client, cursor="Agent-Example_01", limit=5) + + client.http_client.get.assert_awaited_once_with( + "/api/v1/agents", + params={"limit": 5, "cursor": "agent-example_01"}, + ) + + +@pytest.mark.asyncio +async def test_assign_policy_rejects_invalid_agent_name() -> 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) + with pytest.raises(ValueError, match="at least 10 characters"): + await policies.assign_policy_to_agent(client, "short", policy_id=1) client.http_client.post.assert_not_called() @pytest.mark.asyncio -async def test_get_agent_accepts_uuid_object() -> None: +async def test_get_agent_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) + + agent_name = "Agent-Example_01" + await agents.get_agent(client, agent_name) + + client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01") + + +@pytest.mark.asyncio +async def test_get_agent_policy_normalizes_agent_name() -> 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) + await agents.get_agent_policy(client, "Agent-Example_01") + + client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy") + + +@pytest.mark.asyncio +async def test_remove_agent_policy_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock(return_value=DummyResponse()) + + await agents.remove_agent_policy(client, "Agent-Example_01") - client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_id}") + client.http_client.delete.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy") diff --git a/sdks/python/tests/test_control_decorators.py b/sdks/python/tests/test_control_decorators.py index 40eea1e4..5ff5c35f 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 = "550e8400-e29b-41d4-a716-446655440000" + agent.agent_name = "550e8400-e29b-41d4-a716-446655440000" return agent @@ -184,14 +184,14 @@ async def test_calls_pre_and_post(self, mock_agent, mock_safe_response): call_stages = [] async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): call_stages.append(stage) return mock_safe_response @@ -214,14 +214,14 @@ async def test_pre_block_prevents_execution(self, mock_agent, mock_safe_response function_executed = False async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): if stage == "pre": return mock_unsafe_response @@ -247,14 +247,14 @@ async def test_post_check_receives_output(self, mock_agent, mock_safe_response): captured_step = {} async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): if stage == "post": captured_step.update(step) @@ -286,14 +286,14 @@ async def test_extracts_input_param(self, mock_agent, mock_safe_response): captured_step = {} async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): if stage == "pre": captured_step.update(step) @@ -316,14 +316,14 @@ async def test_extracts_message_param(self, mock_agent, mock_safe_response): captured_step = {} async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): if stage == "pre": captured_step.update(step) @@ -346,14 +346,14 @@ async def test_extracts_query_param(self, mock_agent, mock_safe_response): captured_step = {} async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): if stage == "pre": captured_step.update(step) @@ -483,14 +483,14 @@ async def test_custom_step_name_used_in_payload(self, mock_agent, mock_safe_resp captured_steps = [] async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): captured_steps.append(step) return mock_safe_response @@ -521,14 +521,14 @@ async def test_default_step_name_uses_function_name(self, mock_agent, mock_safe_ captured_steps = [] async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): captured_steps.append(step) return mock_safe_response @@ -559,14 +559,14 @@ async def test_step_name_with_tool_decorator(self, mock_agent, mock_safe_respons captured_steps = [] async def mock_evaluate( - agent_uuid, + agent_name, step, stage, server_url, trace_id=None, span_id=None, controls=None, - agent_name=None, + event_agent_name=None, ): captured_steps.append(step) return mock_safe_response diff --git a/sdks/python/tests/test_evaluation.py b/sdks/python/tests/test_evaluation.py index 6bd50101..f1c8c8a5 100644 --- a/sdks/python/tests/test_evaluation.py +++ b/sdks/python/tests/test_evaluation.py @@ -20,9 +20,45 @@ async def test_check_evaluation_requires_step_name_without_models(monkeypatch): with pytest.raises(ValueError, match="step.name is required"): await evaluation.check_evaluation( client=client, - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name=UUID("00000000-0000-0000-0000-000000000001"), step={"type": "llm", "input": "hello"}, stage="pre", ) client.http_client.post.assert_not_called() + + +@pytest.mark.asyncio +async def test_check_evaluation_returns_runtime_fallback_without_models(monkeypatch): + """Fallback path should return an object with expected attributes.""" + monkeypatch.setattr(evaluation, "MODELS_AVAILABLE", False) + + class DummyResponse: + def raise_for_status(self) -> None: + return None + + def json(self) -> dict[str, object]: + return {"is_safe": True, "confidence": 0.75, "reason": "ok"} + + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock(return_value=DummyResponse()) + + result = await evaluation.check_evaluation( + client=client, + agent_name="Agent-Example_01", + step={"type": "llm", "name": "chat", "input": "hello"}, + stage="pre", + ) + + assert result.is_safe is True + assert result.confidence == 0.75 + assert result.reason == "ok" + client.http_client.post.assert_awaited_once_with( + "/api/v1/evaluation", + json={ + "agent_name": "agent-example_01", + "step": {"type": "llm", "name": "chat", "input": "hello"}, + "stage": "pre", + }, + ) diff --git a/sdks/python/tests/test_init_conflict.py b/sdks/python/tests/test_init_conflict.py index 3e712e01..3fb23e14 100644 --- a/sdks/python/tests/test_init_conflict.py +++ b/sdks/python/tests/test_init_conflict.py @@ -19,7 +19,7 @@ def _make_conflict_error() -> httpx.HTTPStatusError: ) -def test_init_surfaces_uuid_conflict() -> None: +def test_init_surfaces_conflict_response() -> None: conflict = _make_conflict_error() with patch( @@ -31,7 +31,6 @@ def test_init_surfaces_uuid_conflict() -> None: ): with pytest.raises(httpx.HTTPStatusError): agent_control.init( - agent_name="Init Conflict Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", agent_description="Testing init conflict handling", ) diff --git a/sdks/python/tests/test_init_step_merge.py b/sdks/python/tests/test_init_step_merge.py index d56fe0d7..142c1fcf 100644 --- a/sdks/python/tests/test_init_step_merge.py +++ b/sdks/python/tests/test_init_step_merge.py @@ -63,8 +63,7 @@ def auto_llm(query: str) -> str: ): with caplog.at_level(logging.WARNING): agent_control.init( - agent_name="Init Merge Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", steps=explicit_steps, ) @@ -102,8 +101,7 @@ async def auto_chat(message: str, temperature: float = 0.2) -> str: new=register_agent_mock, ): agent_control.init( - agent_name="Auto Discovery Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", ) # Then register_agent() receives the auto-derived step schema payload. @@ -145,8 +143,7 @@ async def unresolved(payload: DoesNotExist) -> str: ): with caplog.at_level(logging.WARNING): agent_control.init( - agent_name="Fallback Warning Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", ) # Then initialization continues, using fallback schemas and emitting a warning. @@ -165,6 +162,30 @@ async def unresolved(payload: DoesNotExist) -> str: assert "failed to resolve type hints" in caplog.text +def test_init_logs_agent_updated_when_registration_already_exists( + caplog: pytest.LogCaptureFixture, +) -> None: + # Given a server response indicating this init call updated an existing agent. + agent_name = f"agent-{uuid4().hex[:12]}" + register_agent_mock = AsyncMock(return_value={"created": False, "controls": []}) + health_check_mock = AsyncMock(return_value={"status": "healthy"}) + + # When init() runs registration. + with patch( + "agent_control.__init__.AgentControlClient.health_check", + new=health_check_mock, + ), patch( + "agent_control.__init__.agents.register_agent", + new=register_agent_mock, + ): + with caplog.at_level(logging.INFO): + agent_control.init(agent_name=agent_name) + + # Then the SDK emits the "updated" log branch. + assert "Agent updated" in caplog.text + assert agent_name in caplog.text + + @pytest.mark.asyncio async def test_refresh_controls_uses_strict_conflict_mode() -> None: # Given: an initialized SDK agent session with network-facing calls mocked. @@ -179,8 +200,7 @@ async def test_refresh_controls_uses_strict_conflict_mode() -> None: new=register_agent_mock, ): agent_control.init( - agent_name="Refresh Strict Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", ) # When: controls are refreshed through refresh_controls_async(). diff --git a/sdks/python/tests/test_init_validation.py b/sdks/python/tests/test_init_validation.py index 80341e3f..241ab8b4 100644 --- a/sdks/python/tests/test_init_validation.py +++ b/sdks/python/tests/test_init_validation.py @@ -8,9 +8,9 @@ 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") +def test_init_rejects_invalid_agent_name() -> None: + with pytest.raises(ValueError, match="at least 10 characters"): + agent_control.init(agent_name="short") def test_init_exports_control_scope() -> None: diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index f3208a53..f0152b3e 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -32,11 +32,8 @@ async def test_agent_registration_workflow( from agent_control_models import Agent - # Generate a proper UUID4 for the agent - agent_uuid = uuid.uuid4() - unique_name = f"Integration Test Agent {uuid.uuid4().hex[:8]}" + unique_name = f"agent-{uuid.uuid4().hex[:12]}" agent = Agent( - agent_id=agent_uuid, agent_name=unique_name, agent_description="Testing agent registration", agent_created_at=datetime.now(UTC).isoformat(), @@ -80,10 +77,10 @@ async def test_agent_retrieval_workflow( - Response includes agent metadata - Response includes registered steps """ - agent_id = test_agent["agent_id"] + agent_name = test_agent["agent_name"] # Retrieve agent - agent_data = await agent_control.agents.get_agent(client, agent_id) + agent_data = await agent_control.agents.get_agent(client, agent_name) # Verify response structure assert "agent" in agent_data @@ -91,7 +88,7 @@ async def test_agent_retrieval_workflow( # Verify agent metadata agent = agent_data["agent"] - assert agent["agent_id"] == agent_id + assert agent["agent_name"] == agent_name assert agent["agent_name"] is not None assert "agent_description" in agent @@ -148,14 +145,14 @@ async def test_convenience_get_agent_function( - Convenience function works without manual client management - Returns same data as client-based approach """ - agent_id = test_agent["agent_id"] + agent_name = test_agent["agent_name"] # Use convenience function - agent_data = await agent_control.get_agent(agent_id, server_url=server_url, api_key=api_key) + agent_data = await agent_control.get_agent(agent_name, server_url=server_url, api_key=api_key) # Verify response assert "agent" in agent_data - assert agent_data["agent"]["agent_id"] == agent_id + assert agent_data["agent"]["agent_name"] == agent_name print("✓ Convenience function works") @@ -177,8 +174,7 @@ async def test_init_function_workflow( """ # Initialize agent agent = agent_control.init( - agent_name=f"Init Test Agent {test_agent_id}", - agent_id=test_agent_id, + agent_name=test_agent_id, agent_description="Testing init function", agent_version="1.0.0", server_url=server_url, @@ -189,8 +185,8 @@ async def test_init_function_workflow( # Verify agent instance assert agent is not None - assert agent.agent_name == f"Init Test Agent {test_agent_id}" - assert hasattr(agent, "agent_id") + assert agent.agent_name == test_agent_id + assert hasattr(agent, "agent_name") # Verify current_agent() current = agent_control.current_agent() diff --git a/sdks/python/tests/test_local_evaluation.py b/sdks/python/tests/test_local_evaluation.py index 0afc907c..550999ae 100644 --- a/sdks/python/tests/test_local_evaluation.py +++ b/sdks/python/tests/test_local_evaluation.py @@ -10,7 +10,6 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock -from uuid import UUID import pytest @@ -35,9 +34,9 @@ @pytest.fixture -def agent_uuid() -> UUID: - """Test agent UUID.""" - return UUID("00000000-0000-0000-0000-000000000001") +def agent_name() -> str: + """Test agent name.""" + return "agent-000000000001" @pytest.fixture @@ -201,7 +200,7 @@ class TestCheckEvaluationWithLocal: """Tests for check_evaluation_with_local function.""" @pytest.mark.asyncio - async def test_local_only_controls_no_server_call(self, agent_uuid, llm_payload): + async def test_local_only_controls_no_server_call(self, agent_name, llm_payload): """When only local controls exist, server should not be called.""" controls = [ make_control_dict(1, "local_ctrl", execution="sdk", pattern=r"never_match"), @@ -214,7 +213,7 @@ async def test_local_only_controls_no_server_call(self, agent_uuid, llm_payload) result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -227,7 +226,7 @@ async def test_local_only_controls_no_server_call(self, agent_uuid, llm_payload) assert result.is_safe is True @pytest.mark.asyncio - async def test_server_only_controls_calls_server(self, agent_uuid, llm_payload): + async def test_server_only_controls_calls_server(self, agent_name, llm_payload): """When only server controls exist, server should be called.""" controls = [ make_control_dict(1, "server_ctrl", execution="server"), @@ -243,7 +242,7 @@ async def test_server_only_controls_calls_server(self, agent_uuid, llm_payload): result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -256,7 +255,7 @@ async def test_server_only_controls_calls_server(self, agent_uuid, llm_payload): assert result.is_safe is True @pytest.mark.asyncio - async def test_local_deny_short_circuits(self, agent_uuid, llm_payload): + async def test_local_deny_short_circuits(self, agent_name, llm_payload): """Local deny should return immediately without calling server.""" controls = [ # Local control that will match (deny) @@ -272,7 +271,7 @@ async def test_local_deny_short_circuits(self, agent_uuid, llm_payload): result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -288,7 +287,7 @@ async def test_local_deny_short_circuits(self, agent_uuid, llm_payload): assert result.matches[0].control_name == "local_deny" @pytest.mark.asyncio - async def test_mixed_controls_local_passes_then_server(self, agent_uuid, llm_payload): + async def test_mixed_controls_local_passes_then_server(self, agent_name, llm_payload): """When local controls pass, server controls should still be called.""" controls = [ # Local control that won't match @@ -307,7 +306,7 @@ async def test_mixed_controls_local_passes_then_server(self, agent_uuid, llm_pay result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -320,7 +319,7 @@ async def test_mixed_controls_local_passes_then_server(self, agent_uuid, llm_pay assert result.is_safe is True @pytest.mark.asyncio - async def test_no_controls_returns_safe(self, agent_uuid, llm_payload): + async def test_no_controls_returns_safe(self, agent_name, llm_payload): """When no controls exist, result should be safe.""" controls: list[dict[str, Any]] = [] @@ -330,7 +329,7 @@ async def test_no_controls_returns_safe(self, agent_uuid, llm_payload): result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -344,7 +343,7 @@ async def test_no_controls_returns_safe(self, agent_uuid, llm_payload): assert result.confidence == 1.0 @pytest.mark.asyncio - async def test_invalid_local_control_skipped(self, agent_uuid, llm_payload): + async def test_invalid_local_control_skipped(self, agent_name, llm_payload): """Invalid local controls should be skipped.""" controls = [ # Invalid control (missing required fields) @@ -364,7 +363,7 @@ async def test_invalid_local_control_skipped(self, agent_uuid, llm_payload): # Should not raise, just skip invalid control result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -375,7 +374,7 @@ async def test_invalid_local_control_skipped(self, agent_uuid, llm_payload): assert result.is_safe is True @pytest.mark.asyncio - async def test_tool_step_local_evaluation(self, agent_uuid, tool_payload): + async def test_tool_step_local_evaluation(self, agent_name, tool_payload): """Local evaluation should work with Step payloads.""" controls = [ make_control_dict( @@ -393,7 +392,7 @@ async def test_tool_step_local_evaluation(self, agent_uuid, tool_payload): result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=tool_payload, stage="pre", controls=controls, @@ -406,7 +405,7 @@ async def test_tool_step_local_evaluation(self, agent_uuid, tool_payload): assert result.is_safe is False @pytest.mark.asyncio - async def test_mixed_controls_merged_results(self, agent_uuid, llm_payload): + async def test_mixed_controls_merged_results(self, agent_name, llm_payload): """Results from local and server should be merged.""" controls = [ # Local control (action=log, will match but not deny) @@ -435,7 +434,7 @@ async def test_mixed_controls_merged_results(self, agent_uuid, llm_payload): result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -450,7 +449,7 @@ async def test_mixed_controls_merged_results(self, agent_uuid, llm_payload): assert len(result.matches) == 2 @pytest.mark.asyncio - async def test_tool_step_mixed_local_and_server_controls(self, agent_uuid, tool_payload): + async def test_tool_step_mixed_local_and_server_controls(self, agent_name, tool_payload): """Test mixed local/server controls for same tool step. Given: A tool step with both local and server controls @@ -498,7 +497,7 @@ async def test_tool_step_mixed_local_and_server_controls(self, agent_uuid, tool_ result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=tool_payload, stage="pre", controls=controls, @@ -514,7 +513,7 @@ async def test_tool_step_mixed_local_and_server_controls(self, agent_uuid, tool_ assert result.matches[0].control_name == "server_tool_ctrl" @pytest.mark.asyncio - async def test_tool_step_local_deny_skips_server(self, agent_uuid, tool_payload): + async def test_tool_step_local_deny_skips_server(self, agent_name, tool_payload): """Test that local deny on tool step short-circuits server call. Given: A tool step with local deny control that matches @@ -548,7 +547,7 @@ async def test_tool_step_local_deny_skips_server(self, agent_uuid, tool_payload) result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=tool_payload, stage="pre", controls=controls, @@ -564,7 +563,7 @@ async def test_tool_step_local_deny_skips_server(self, agent_uuid, tool_payload) assert result.matches[0].control_name == "local_deny_ctrl" @pytest.mark.asyncio - async def test_local_control_with_missing_evaluator_raises(self, agent_uuid, llm_payload): + async def test_local_control_with_missing_evaluator_raises(self, agent_name, llm_payload): """Test that local control with unavailable evaluator raises RuntimeError. Given: A local control referencing an evaluator that doesn't exist @@ -587,7 +586,7 @@ async def test_local_control_with_missing_evaluator_raises(self, agent_uuid, llm with pytest.raises(RuntimeError) as exc_info: await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -598,7 +597,7 @@ async def test_local_control_with_missing_evaluator_raises(self, agent_uuid, llm assert "not available" in str(exc_info.value) @pytest.mark.asyncio - async def test_local_control_with_agent_scoped_evaluator_raises(self, agent_uuid, llm_payload): + async def test_local_control_with_agent_scoped_evaluator_raises(self, agent_name, llm_payload): """Test that local control with agent-scoped evaluator raises RuntimeError. Given: A local control referencing an agent-scoped evaluator (agent:evaluator) @@ -621,7 +620,7 @@ async def test_local_control_with_agent_scoped_evaluator_raises(self, agent_uuid with pytest.raises(RuntimeError) as exc_info: await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -632,7 +631,7 @@ async def test_local_control_with_agent_scoped_evaluator_raises(self, agent_uuid assert "server-only" in str(exc_info.value) @pytest.mark.asyncio - async def test_server_control_with_missing_evaluator_allowed(self, agent_uuid, llm_payload): + async def test_server_control_with_missing_evaluator_allowed(self, agent_name, llm_payload): """Test that server control with unavailable evaluator is allowed (server handles it). Given: A server control (execution="server") referencing an evaluator that doesn't exist locally @@ -660,7 +659,7 @@ async def test_server_control_with_missing_evaluator_allowed(self, agent_uuid, l # Should not raise - server handles unavailable evaluators result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -671,7 +670,7 @@ async def test_server_control_with_missing_evaluator_allowed(self, agent_uuid, l assert result.is_safe is True @pytest.mark.asyncio - async def test_invalid_local_control_populates_errors(self, agent_uuid, llm_payload): + async def test_invalid_local_control_populates_errors(self, agent_name, llm_payload): """Test that invalid local controls appear in result.errors. Given: A local control that fails validation @@ -689,7 +688,7 @@ async def test_invalid_local_control_populates_errors(self, agent_uuid, llm_payl result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, @@ -708,7 +707,7 @@ async def test_invalid_local_control_populates_errors(self, agent_uuid, llm_payl assert "Failed to parse local control" in (result.errors[0].result.error or "") @pytest.mark.asyncio - async def test_malformed_server_control_still_calls_server(self, agent_uuid, llm_payload): + async def test_malformed_server_control_still_calls_server(self, agent_name, llm_payload): """Test that malformed server control data still triggers server call. Given: A server control (execution="server") with missing/malformed 'control' data @@ -736,7 +735,7 @@ async def test_malformed_server_control_still_calls_server(self, agent_uuid, llm result = await check_evaluation_with_local( client=client, - agent_uuid=agent_uuid, + agent_name=agent_name, step=llm_payload, stage="pre", controls=controls, diff --git a/sdks/python/tests/test_observability.py b/sdks/python/tests/test_observability.py index f02f1437..f983afea 100644 --- a/sdks/python/tests/test_observability.py +++ b/sdks/python/tests/test_observability.py @@ -26,7 +26,7 @@ def create_mock_event(): mock_event.model_dump = MagicMock(return_value={ "trace_id": "a" * 32, "span_id": "b" * 16, - "agent_uuid": str(uuid4()), + "agent_name": str(uuid4()), "agent_name": "test-agent", "control_id": 1, "control_name": "test-control", diff --git a/sdks/python/tests/test_observability_updates.py b/sdks/python/tests/test_observability_updates.py index 3dc33208..a99d6b7a 100644 --- a/sdks/python/tests/test_observability_updates.py +++ b/sdks/python/tests/test_observability_updates.py @@ -146,7 +146,7 @@ def _make_request(self, step_type="llm"): # Tool steps require object input, LLM steps accept string step_input = {"query": "hello"} if step_type == "tool" else "hello" return EvaluationRequest( - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name="agent-000000000001", step={"type": step_type, "name": "test-step", "input": step_input}, stage="pre", ) @@ -262,8 +262,8 @@ def test_fallback_warning_logged_only_once(self): with patch("agent_control.evaluation.is_observability_enabled", return_value=True), \ patch("agent_control.evaluation.add_event"), \ patch("agent_control.evaluation._logger") as mock_logger: - _emit_local_events(response, request, [ctrl], None, None, "a") - _emit_local_events(response, request, [ctrl], None, None, "a") + _emit_local_events(response, request, [ctrl], None, None, "agent-test-a1") + _emit_local_events(response, request, [ctrl], None, None, "agent-test-a1") assert mock_logger.warning.call_count == 1 @@ -323,13 +323,13 @@ async def test_emits_events_when_trace_context_provided(self): patch("agent_control.evaluation._emit_local_events") as mock_emit: result = await evaluation.check_evaluation_with_local( client=client, - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name="agent-000000000001", step=step, stage="pre", controls=controls, trace_id="abc123", span_id="def456", - agent_name="test-agent", + event_agent_name="test-agent", ) mock_emit.assert_called_once() @@ -337,7 +337,7 @@ async def test_emits_events_when_trace_context_provided(self): assert call_args[0][2] is not None # local_controls assert call_args[0][3] == "abc123" # trace_id assert call_args[0][4] == "def456" # span_id - assert call_args[0][5] == "test-agent" # agent_name + assert call_args.kwargs["agent_name"] == "test-agent" # Also verify non_matches propagated assert result.non_matches is not None @@ -375,7 +375,7 @@ async def test_emits_events_without_trace_context(self): patch("agent_control.evaluation._emit_local_events") as mock_emit: await evaluation.check_evaluation_with_local( client=client, - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name="agent-000000000001", step=step, stage="pre", controls=controls, @@ -421,7 +421,7 @@ async def test_forwards_trace_headers_to_server(self): with patch("agent_control.evaluation.list_evaluators", return_value=["regex"]): await evaluation.check_evaluation_with_local( client=client, - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name="agent-000000000001", step=step, stage="pre", controls=controls, @@ -472,7 +472,6 @@ async def test_non_matches_populated_in_stats(self): } ctx = ControlContext( - agent_uuid="test-uuid", agent_name="test-agent", server_url="http://localhost:8000", func=lambda: None, diff --git a/sdks/python/tests/test_validation.py b/sdks/python/tests/test_validation.py index 1564f78b..04eb2398 100644 --- a/sdks/python/tests/test_validation.py +++ b/sdks/python/tests/test_validation.py @@ -1,42 +1,24 @@ """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") +from agent_control.validation import ensure_agent_name -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_agent_name_normalizes_to_lowercase() -> None: + assert ensure_agent_name("Agent-Name_123") == "agent-name_123" -def test_ensure_uuid_str_returns_string() -> None: - value = uuid4() - assert ensure_uuid_str(value) == str(value) +def test_ensure_agent_name_rejects_too_short() -> None: + with pytest.raises(ValueError, match="at least 10 characters"): + ensure_agent_name("short") -def test_ensure_uuid_str_accepts_uuid_string() -> None: - value = str(uuid4()) - assert ensure_uuid_str(value) == str(UUID(value)) +def test_ensure_agent_name_rejects_invalid_characters() -> None: + with pytest.raises(ValueError, match="may only contain"): + ensure_agent_name("agent name with spaces") -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") +def test_ensure_agent_name_respects_field_name() -> None: + with pytest.raises(ValueError, match="custom_field must be at least 10 characters"): + ensure_agent_name("small", field_name="custom_field") diff --git a/sdks/typescript/overlays/method-names.overlay.yaml b/sdks/typescript/overlays/method-names.overlay.yaml index 3fd730c7..fd4927f7 100644 --- a/sdks/typescript/overlays/method-names.overlay.yaml +++ b/sdks/typescript/overlays/method-names.overlay.yaml @@ -15,42 +15,42 @@ actions: x-speakeasy-group: agents x-speakeasy-name-override: init - - target: $["paths"]["/api/v1/agents/{agent_id}"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_name}"]["get"] update: x-speakeasy-group: agents x-speakeasy-name-override: get - - target: $["paths"]["/api/v1/agents/{agent_id}"]["patch"] + - target: $["paths"]["/api/v1/agents/{agent_name}"]["patch"] update: x-speakeasy-group: agents x-speakeasy-name-override: update - - target: $["paths"]["/api/v1/agents/{agent_id}/controls"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_name}/controls"]["get"] update: x-speakeasy-group: agents x-speakeasy-name-override: listControls - - target: $["paths"]["/api/v1/agents/{agent_id}/evaluators"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_name}/evaluators"]["get"] update: x-speakeasy-group: agents x-speakeasy-name-override: listEvaluators - - target: $["paths"]["/api/v1/agents/{agent_id}/evaluators/{evaluator_name}"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_name}/evaluators/{evaluator_name}"]["get"] update: x-speakeasy-group: agents x-speakeasy-name-override: getEvaluator - - target: $["paths"]["/api/v1/agents/{agent_id}/policy"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy"]["get"] update: x-speakeasy-group: agents x-speakeasy-name-override: getPolicy - - target: $["paths"]["/api/v1/agents/{agent_id}/policy"]["delete"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy"]["delete"] update: x-speakeasy-group: agents x-speakeasy-name-override: deletePolicy - - target: $["paths"]["/api/v1/agents/{agent_id}/policy/{policy_id}"]["post"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy/{policy_id}"]["post"] update: x-speakeasy-group: agents x-speakeasy-name-override: updatePolicy diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index 2836f23f..f9cd94d5 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -9,7 +9,6 @@ export type APIKeyProvider = string | (() => Promise); export interface AgentControlInitOptions { agentName: string; - agentId?: string; serverUrl: string; apiKey?: APIKeyProvider; steps?: StepSchema[]; diff --git a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts index 9251bfd9..2e8a18b6 100644 --- a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js"; * The agent will no longer have any protection controls active. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -48,7 +48,7 @@ import { Result } from "../types/fp.js"; */ export function agentsDeletePolicy( client: AgentControlSDKCore, - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + request: operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, ): APIPromise< Result< @@ -73,7 +73,7 @@ export function agentsDeletePolicy( async function $do( client: AgentControlSDKCore, - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + request: operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, ): Promise< [ @@ -97,7 +97,7 @@ async function $do( (value) => z.parse( operations - .DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$outboundSchema, + .DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$outboundSchema, value, ), "Input validation failed", @@ -109,13 +109,13 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/policy")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -128,7 +128,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "delete_agent_policy_api_v1_agents__agent_id__policy_delete", + operationID: "delete_agent_policy_api_v1_agents__agent_name__policy_delete", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-get-evaluator.ts b/sdks/typescript/src/generated/funcs/agents-get-evaluator.ts index ecfff7ae..5404195d 100644 --- a/sdks/typescript/src/generated/funcs/agents-get-evaluator.ts +++ b/sdks/typescript/src/generated/funcs/agents-get-evaluator.ts @@ -34,7 +34,7 @@ import { Result } from "../types/fp.js"; * Get a specific evaluator schema registered with an agent. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * evaluator_name: Name of the evaluator * db: Database session (injected) * @@ -47,7 +47,7 @@ import { Result } from "../types/fp.js"; export function agentsGetEvaluator( client: AgentControlSDKCore, request: - operations.GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, + operations.GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -73,7 +73,7 @@ export function agentsGetEvaluator( async function $do( client: AgentControlSDKCore, request: - operations.GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, + operations.GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, options?: RequestOptions, ): Promise< [ @@ -97,7 +97,7 @@ async function $do( (value) => z.parse( operations - .GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest$outboundSchema, + .GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest$outboundSchema, value, ), "Input validation failed", @@ -109,7 +109,7 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), @@ -120,7 +120,7 @@ async function $do( }; const path = pathToFunc( - "/api/v1/agents/{agent_id}/evaluators/{evaluator_name}", + "/api/v1/agents/{agent_name}/evaluators/{evaluator_name}", )(pathParams); const headers = new Headers(compactMap({ @@ -135,7 +135,7 @@ async function $do( options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", operationID: - "get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get", + "get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-get-policy.ts b/sdks/typescript/src/generated/funcs/agents-get-policy.ts index ca3c7a92..cbe953f3 100644 --- a/sdks/typescript/src/generated/funcs/agents-get-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-get-policy.ts @@ -34,7 +34,7 @@ import { Result } from "../types/fp.js"; * Retrieve the policy currently assigned to an agent. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -45,7 +45,7 @@ import { Result } from "../types/fp.js"; */ export function agentsGetPolicy( client: AgentControlSDKCore, - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -70,7 +70,7 @@ export function agentsGetPolicy( async function $do( client: AgentControlSDKCore, - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, ): Promise< [ @@ -94,7 +94,7 @@ async function $do( (value) => z.parse( operations - .GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$outboundSchema, + .GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$outboundSchema, value, ), "Input validation failed", @@ -106,13 +106,13 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/policy")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -125,7 +125,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "get_agent_policy_api_v1_agents__agent_id__policy_get", + operationID: "get_agent_policy_api_v1_agents__agent_name__policy_get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-get.ts b/sdks/typescript/src/generated/funcs/agents-get.ts index e054ae4b..e650726a 100644 --- a/sdks/typescript/src/generated/funcs/agents-get.ts +++ b/sdks/typescript/src/generated/funcs/agents-get.ts @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js"; * Returns the latest version of each step (deduplicated by type+name). * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -48,7 +48,7 @@ import { Result } from "../types/fp.js"; */ export function agentsGet( client: AgentControlSDKCore, - request: operations.GetAgentApiV1AgentsAgentIdGetRequest, + request: operations.GetAgentApiV1AgentsAgentNameGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -73,7 +73,7 @@ export function agentsGet( async function $do( client: AgentControlSDKCore, - request: operations.GetAgentApiV1AgentsAgentIdGetRequest, + request: operations.GetAgentApiV1AgentsAgentNameGetRequest, options?: RequestOptions, ): Promise< [ @@ -96,7 +96,7 @@ async function $do( request, (value) => z.parse( - operations.GetAgentApiV1AgentsAgentIdGetRequest$outboundSchema, + operations.GetAgentApiV1AgentsAgentNameGetRequest$outboundSchema, value, ), "Input validation failed", @@ -108,13 +108,13 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -127,7 +127,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "get_agent_api_v1_agents__agent_id__get", + operationID: "get_agent_api_v1_agents__agent_name__get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-init.ts b/sdks/typescript/src/generated/funcs/agents-init.ts index 9937275c..1cb02298 100644 --- a/sdks/typescript/src/generated/funcs/agents-init.ts +++ b/sdks/typescript/src/generated/funcs/agents-init.ts @@ -34,9 +34,7 @@ import { Result } from "../types/fp.js"; * * This endpoint is idempotent: * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists with the same UUID, updates registration data - * - If the agent name exists with a different UUID, returns 409 Conflict - * - If the UUID exists with a different name, returns 409 Conflict (no renames) + * - If the agent name exists, updates registration data in place * * conflict_mode controls registration conflict handling: * - strict (default): preserve compatibility checks and conflict errors @@ -48,10 +46,6 @@ import { Result } from "../types/fp.js"; * * Returns: * InitAgentResponse with created flag and active controls (if policy assigned) - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update */ export function agentsInit( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/funcs/agents-list-controls.ts b/sdks/typescript/src/generated/funcs/agents-list-controls.ts index 764e550c..b44c3402 100644 --- a/sdks/typescript/src/generated/funcs/agents-list-controls.ts +++ b/sdks/typescript/src/generated/funcs/agents-list-controls.ts @@ -37,7 +37,7 @@ import { Result } from "../types/fp.js"; * Returns an empty list if the agent has no policy. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -48,7 +48,7 @@ import { Result } from "../types/fp.js"; */ export function agentsListControls( client: AgentControlSDKCore, - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -73,7 +73,7 @@ export function agentsListControls( async function $do( client: AgentControlSDKCore, - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): Promise< [ @@ -97,7 +97,7 @@ async function $do( (value) => z.parse( operations - .ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$outboundSchema, + .ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$outboundSchema, value, ), "Input validation failed", @@ -109,13 +109,13 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/controls")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/controls")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -128,7 +128,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "list_agent_controls_api_v1_agents__agent_id__controls_get", + operationID: "list_agent_controls_api_v1_agents__agent_name__controls_get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-list-evaluators.ts b/sdks/typescript/src/generated/funcs/agents-list-evaluators.ts index fd1de814..bded3726 100644 --- a/sdks/typescript/src/generated/funcs/agents-list-evaluators.ts +++ b/sdks/typescript/src/generated/funcs/agents-list-evaluators.ts @@ -38,7 +38,7 @@ import { Result } from "../types/fp.js"; * - UI to display available config options * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * cursor: Optional cursor for pagination (name of last evaluator from previous page) * limit: Pagination limit (default 20, max 100) * db: Database session (injected) @@ -51,7 +51,8 @@ import { Result } from "../types/fp.js"; */ export function agentsListEvaluators( client: AgentControlSDKCore, - request: operations.ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest, + request: + operations.ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -76,7 +77,8 @@ export function agentsListEvaluators( async function $do( client: AgentControlSDKCore, - request: operations.ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest, + request: + operations.ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest, options?: RequestOptions, ): Promise< [ @@ -100,7 +102,7 @@ async function $do( (value) => z.parse( operations - .ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest$outboundSchema, + .ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest$outboundSchema, value, ), "Input validation failed", @@ -112,13 +114,13 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/evaluators")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/evaluators")(pathParams); const query = encodeFormQuery({ "cursor": payload.cursor, @@ -137,7 +139,7 @@ async function $do( options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", operationID: - "list_agent_evaluators_api_v1_agents__agent_id__evaluators_get", + "list_agent_evaluators_api_v1_agents__agent_name__evaluators_get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-list.ts b/sdks/typescript/src/generated/funcs/agents-list.ts index 65d12ac0..82e21470 100644 --- a/sdks/typescript/src/generated/funcs/agents-list.ts +++ b/sdks/typescript/src/generated/funcs/agents-list.ts @@ -33,11 +33,11 @@ import { Result } from "../types/fp.js"; * @remarks * List all registered agents with cursor-based pagination. * - * Returns a summary of each agent including ID, name, policy assignment, + * Returns a summary of each agent including identifier, policy assignment, * and counts of registered steps and evaluators. * * Args: - * cursor: Optional cursor for pagination (UUID of last agent from previous page) + * cursor: Optional cursor for pagination (last agent name from previous page) * limit: Pagination limit (default 20, max 100) * name: Optional name filter (case-insensitive partial match) * db: Database session (injected) diff --git a/sdks/typescript/src/generated/funcs/agents-update-policy.ts b/sdks/typescript/src/generated/funcs/agents-update-policy.ts index ef8fbc68..7626a798 100644 --- a/sdks/typescript/src/generated/funcs/agents-update-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-update-policy.ts @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js"; * The agent will immediately inherit all controls from the assigned policy. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * policy_id: ID of the policy to assign * db: Database session (injected) * @@ -49,7 +49,8 @@ import { Result } from "../types/fp.js"; */ export function agentsUpdatePolicy( client: AgentControlSDKCore, - request: operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + request: + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, ): APIPromise< Result< @@ -74,7 +75,8 @@ export function agentsUpdatePolicy( async function $do( client: AgentControlSDKCore, - request: operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + request: + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, ): Promise< [ @@ -98,7 +100,7 @@ async function $do( (value) => z.parse( operations - .SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$outboundSchema, + .SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$outboundSchema, value, ), "Input validation failed", @@ -110,7 +112,7 @@ async function $do( const body = null; const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), @@ -120,7 +122,7 @@ async function $do( }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy/{policy_id}")( + const path = pathToFunc("/api/v1/agents/{agent_name}/policy/{policy_id}")( pathParams, ); @@ -136,7 +138,7 @@ async function $do( options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", operationID: - "set_agent_policy_api_v1_agents__agent_id__policy__policy_id__post", + "set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/agents-update.ts b/sdks/typescript/src/generated/funcs/agents-update.ts index 1b5aed31..e82644cf 100644 --- a/sdks/typescript/src/generated/funcs/agents-update.ts +++ b/sdks/typescript/src/generated/funcs/agents-update.ts @@ -37,7 +37,7 @@ import { Result } from "../types/fp.js"; * Removals are idempotent - attempting to remove non-existent items is not an error. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * request: Lists of step/evaluator identifiers to remove * db: Database session (injected) * @@ -50,7 +50,7 @@ import { Result } from "../types/fp.js"; */ export function agentsUpdate( client: AgentControlSDKCore, - request: operations.PatchAgentApiV1AgentsAgentIdPatchRequest, + request: operations.PatchAgentApiV1AgentsAgentNamePatchRequest, options?: RequestOptions, ): APIPromise< Result< @@ -75,7 +75,7 @@ export function agentsUpdate( async function $do( client: AgentControlSDKCore, - request: operations.PatchAgentApiV1AgentsAgentIdPatchRequest, + request: operations.PatchAgentApiV1AgentsAgentNamePatchRequest, options?: RequestOptions, ): Promise< [ @@ -98,7 +98,7 @@ async function $do( request, (value) => z.parse( - operations.PatchAgentApiV1AgentsAgentIdPatchRequest$outboundSchema, + operations.PatchAgentApiV1AgentsAgentNamePatchRequest$outboundSchema, value, ), "Input validation failed", @@ -110,13 +110,13 @@ async function $do( const body = encodeJSON("body", payload.body, { explode: true }); const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { + agent_name: encodeSimple("agent_name", payload.agent_name, { explode: false, charEncoding: "percent", }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}")(pathParams); const headers = new Headers(compactMap({ "Content-Type": "application/json", @@ -130,7 +130,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "patch_agent_api_v1_agents__agent_id__patch", + operationID: "patch_agent_api_v1_agents__agent_name__patch", oAuth2Scopes: null, resolvedSecurity: requestSecurity, diff --git a/sdks/typescript/src/generated/funcs/evaluators-list.ts b/sdks/typescript/src/generated/funcs/evaluators-list.ts index 5a59ce8c..2f378a5f 100644 --- a/sdks/typescript/src/generated/funcs/evaluators-list.ts +++ b/sdks/typescript/src/generated/funcs/evaluators-list.ts @@ -38,7 +38,7 @@ import { Result } from "../types/fp.js"; * - **sql**: SQL query validation * * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. */ export function evaluatorsList( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/funcs/observability-get-control-stats.ts b/sdks/typescript/src/generated/funcs/observability-get-control-stats.ts index dbb0cf69..0a3b349f 100644 --- a/sdks/typescript/src/generated/funcs/observability-get-control-stats.ts +++ b/sdks/typescript/src/generated/funcs/observability-get-control-stats.ts @@ -37,7 +37,7 @@ import { Result } from "../types/fp.js"; * * Args: * control_id: Control ID to get stats for - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) @@ -121,7 +121,7 @@ async function $do( ); const query = encodeFormQuery({ - "agent_uuid": payload.agent_uuid, + "agent_name": payload.agent_name, "include_timeseries": payload.include_timeseries, "time_range": payload.time_range, }); diff --git a/sdks/typescript/src/generated/funcs/observability-get-stats.ts b/sdks/typescript/src/generated/funcs/observability-get-stats.ts index f209a135..fb3fcd44 100644 --- a/sdks/typescript/src/generated/funcs/observability-get-stats.ts +++ b/sdks/typescript/src/generated/funcs/observability-get-stats.ts @@ -37,7 +37,7 @@ import { Result } from "../types/fp.js"; * Use /stats/controls/{control_id} for single control stats. * * Args: - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) @@ -109,7 +109,7 @@ async function $do( const path = pathToFunc("/api/v1/observability/stats")(); const query = encodeFormQuery({ - "agent_uuid": payload.agent_uuid, + "agent_name": payload.agent_name, "include_timeseries": payload.include_timeseries, "time_range": payload.time_range, }); diff --git a/sdks/typescript/src/generated/funcs/observability-query-events.ts b/sdks/typescript/src/generated/funcs/observability-query-events.ts index 1dda335f..f61b92f0 100644 --- a/sdks/typescript/src/generated/funcs/observability-query-events.ts +++ b/sdks/typescript/src/generated/funcs/observability-query-events.ts @@ -36,7 +36,7 @@ import { Result } from "../types/fp.js"; * - trace_id: Get all events for a request * - span_id: Get all events for a function call * - control_execution_id: Get a specific event - * - agent_uuid: Filter by agent + * - agent_name: Filter by agent * - control_ids: Filter by controls * - actions: Filter by actions (allow, deny, warn, log) * - matched: Filter by matched status diff --git a/sdks/typescript/src/generated/models/agent-ref.ts b/sdks/typescript/src/generated/models/agent-ref.ts index 765408c2..e24952df 100644 --- a/sdks/typescript/src/generated/models/agent-ref.ts +++ b/sdks/typescript/src/generated/models/agent-ref.ts @@ -14,11 +14,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type AgentRef = { /** - * Agent UUID - */ - agentId: string; - /** - * Agent name + * Agent identifier */ agentName: string; }; @@ -26,12 +22,10 @@ export type AgentRef = { /** @internal */ export const AgentRef$inboundSchema: z.ZodMiniType = z.pipe( z.object({ - agent_id: types.string(), agent_name: types.string(), }), z.transform((v) => { return remap$(v, { - "agent_id": "agentId", "agent_name": "agentName", }); }), diff --git a/sdks/typescript/src/generated/models/agent-summary.ts b/sdks/typescript/src/generated/models/agent-summary.ts index 9915c153..a2cccd70 100644 --- a/sdks/typescript/src/generated/models/agent-summary.ts +++ b/sdks/typescript/src/generated/models/agent-summary.ts @@ -18,11 +18,7 @@ export type AgentSummary = { */ activeControlsCount: number; /** - * UUID of the agent - */ - agentId: string; - /** - * Human-readable name of the agent + * Unique identifier of the agent */ agentName: string; /** @@ -48,7 +44,6 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = z.pipe( z.object({ active_controls_count: z._default(types.number(), 0), - agent_id: types.string(), agent_name: types.string(), created_at: z.optional(z.nullable(types.string())), evaluator_count: z._default(types.number(), 0), @@ -58,7 +53,6 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = z.transform((v) => { return remap$(v, { "active_controls_count": "activeControlsCount", - "agent_id": "agentId", "agent_name": "agentName", "created_at": "createdAt", "evaluator_count": "evaluatorCount", diff --git a/sdks/typescript/src/generated/models/agent.ts b/sdks/typescript/src/generated/models/agent.ts index 0eb4d3ad..3155c4d7 100644 --- a/sdks/typescript/src/generated/models/agent.ts +++ b/sdks/typescript/src/generated/models/agent.ts @@ -15,7 +15,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; * @remarks * * An agent represents an AI system that can be protected and monitored. - * Each agent has a unique ID and can have multiple steps registered with it. + * Each agent has a unique immutable name and can have multiple steps registered with it. */ export type Agent = { /** @@ -26,16 +26,12 @@ export type Agent = { * Optional description of the agent's purpose */ agentDescription?: string | null | undefined; - /** - * Unique identifier for the agent (UUID format) - */ - agentId: string; /** * Free-form metadata dictionary for custom properties */ agentMetadata?: { [k: string]: any } | null | undefined; /** - * Human-readable name for the agent + * Unique immutable identifier for the agent */ agentName: string; /** @@ -53,7 +49,6 @@ export const Agent$inboundSchema: z.ZodMiniType = z.pipe( z.object({ agent_created_at: z.optional(z.nullable(types.string())), agent_description: z.optional(z.nullable(types.string())), - agent_id: types.string(), agent_metadata: z.optional(z.nullable(z.record(z.string(), z.any()))), agent_name: types.string(), agent_updated_at: z.optional(z.nullable(types.string())), @@ -63,7 +58,6 @@ export const Agent$inboundSchema: z.ZodMiniType = z.pipe( return remap$(v, { "agent_created_at": "agentCreatedAt", "agent_description": "agentDescription", - "agent_id": "agentId", "agent_metadata": "agentMetadata", "agent_name": "agentName", "agent_updated_at": "agentUpdatedAt", @@ -75,7 +69,6 @@ export const Agent$inboundSchema: z.ZodMiniType = z.pipe( export type Agent$Outbound = { agent_created_at?: string | null | undefined; agent_description?: string | null | undefined; - agent_id: string; agent_metadata?: { [k: string]: any } | null | undefined; agent_name: string; agent_updated_at?: string | null | undefined; @@ -88,7 +81,6 @@ export const Agent$outboundSchema: z.ZodMiniType = z z.object({ agentCreatedAt: z.optional(z.nullable(z.string())), agentDescription: z.optional(z.nullable(z.string())), - agentId: z.string(), agentMetadata: z.optional(z.nullable(z.record(z.string(), z.any()))), agentName: z.string(), agentUpdatedAt: z.optional(z.nullable(z.string())), @@ -98,7 +90,6 @@ export const Agent$outboundSchema: z.ZodMiniType = z return remap$(v, { agentCreatedAt: "agent_created_at", agentDescription: "agent_description", - agentId: "agent_id", agentMetadata: "agent_metadata", agentName: "agent_name", agentUpdatedAt: "agent_updated_at", diff --git a/sdks/typescript/src/generated/models/control-execution-event.ts b/sdks/typescript/src/generated/models/control-execution-event.ts index 5feddb49..c5eef756 100644 --- a/sdks/typescript/src/generated/models/control-execution-event.ts +++ b/sdks/typescript/src/generated/models/control-execution-event.ts @@ -69,8 +69,7 @@ export type CheckStage = OpenEnum; * control_execution_id: Unique ID for this specific control execution * trace_id: OpenTelemetry-compatible trace ID (128-bit hex, 32 chars) * span_id: OpenTelemetry-compatible span ID (64-bit hex, 16 chars) - * agent_uuid: UUID of the agent that executed the control - * agent_name: Name of the agent (denormalized for queries) + * agent_name: Identifier of the agent that executed the control * control_id: Database ID of the control * control_name: Name of the control (denormalized for queries) * check_stage: "pre" (before execution) or "post" (after execution) @@ -91,13 +90,9 @@ export type ControlExecutionEvent = { */ action: ControlExecutionEventAction; /** - * Name of the agent (denormalized) + * Identifier of the agent */ agentName: string; - /** - * UUID of the agent - */ - agentUuid: string; /** * Type of call: 'llm_call' or 'tool_call' */ @@ -197,7 +192,6 @@ export const ControlExecutionEvent$inboundSchema: z.ZodMiniType< z.object({ action: ControlExecutionEventAction$inboundSchema, agent_name: types.string(), - agent_uuid: types.string(), applies_to: ControlExecutionEventAppliesTo$inboundSchema, check_stage: CheckStage$inboundSchema, confidence: types.number(), @@ -217,7 +211,6 @@ export const ControlExecutionEvent$inboundSchema: z.ZodMiniType< z.transform((v) => { return remap$(v, { "agent_name": "agentName", - "agent_uuid": "agentUuid", "applies_to": "appliesTo", "check_stage": "checkStage", "control_execution_id": "controlExecutionId", @@ -236,7 +229,6 @@ export const ControlExecutionEvent$inboundSchema: z.ZodMiniType< export type ControlExecutionEvent$Outbound = { action: string; agent_name: string; - agent_uuid: string; applies_to: string; check_stage: string; confidence: number; @@ -262,7 +254,6 @@ export const ControlExecutionEvent$outboundSchema: z.ZodMiniType< z.object({ action: ControlExecutionEventAction$outboundSchema, agentName: z.string(), - agentUuid: z.string(), appliesTo: ControlExecutionEventAppliesTo$outboundSchema, checkStage: CheckStage$outboundSchema, confidence: z.number(), @@ -282,7 +273,6 @@ export const ControlExecutionEvent$outboundSchema: z.ZodMiniType< z.transform((v) => { return remap$(v, { agentName: "agent_name", - agentUuid: "agent_uuid", appliesTo: "applies_to", checkStage: "check_stage", controlExecutionId: "control_execution_id", diff --git a/sdks/typescript/src/generated/models/control-stats-response.ts b/sdks/typescript/src/generated/models/control-stats-response.ts index 619ccf44..b52c61f8 100644 --- a/sdks/typescript/src/generated/models/control-stats-response.ts +++ b/sdks/typescript/src/generated/models/control-stats-response.ts @@ -18,7 +18,7 @@ import { StatsTotals, StatsTotals$inboundSchema } from "./stats-totals.js"; * Contains stats for a single control (with optional timeseries). * * Attributes: - * agent_uuid: Agent UUID + * agent_name: Agent identifier * time_range: Time range used * control_id: Control ID * control_name: Control name @@ -26,9 +26,9 @@ import { StatsTotals, StatsTotals$inboundSchema } from "./stats-totals.js"; */ export type ControlStatsResponse = { /** - * Agent UUID + * Agent identifier */ - agentUuid: string; + agentName: string; /** * Control ID */ @@ -68,7 +68,7 @@ export const ControlStatsResponse$inboundSchema: z.ZodMiniType< unknown > = z.pipe( z.object({ - agent_uuid: types.string(), + agent_name: types.string(), control_id: types.number(), control_name: types.string(), stats: StatsTotals$inboundSchema, @@ -76,7 +76,7 @@ export const ControlStatsResponse$inboundSchema: z.ZodMiniType< }), z.transform((v) => { return remap$(v, { - "agent_uuid": "agentUuid", + "agent_name": "agentName", "control_id": "controlId", "control_name": "controlName", "time_range": "timeRange", diff --git a/sdks/typescript/src/generated/models/evaluation-request.ts b/sdks/typescript/src/generated/models/evaluation-request.ts index 2b5ec0ef..231d4c94 100644 --- a/sdks/typescript/src/generated/models/evaluation-request.ts +++ b/sdks/typescript/src/generated/models/evaluation-request.ts @@ -28,15 +28,15 @@ export type Stage = ClosedEnum; * policy compliance, and control rules. * * Attributes: - * agent_uuid: UUID of the agent making the request + * agent_name: Unique identifier of the agent making the request * step: Step payload for evaluation * stage: 'pre' (before execution) or 'post' (after execution) */ export type EvaluationRequest = { /** - * UUID of the agent making the evaluation request + * Identifier of the agent making the evaluation request */ - agentUuid: string; + agentName: string; /** * Evaluation stage: 'pre' or 'post' */ @@ -52,7 +52,7 @@ export const Stage$outboundSchema: z.ZodMiniEnum = z.enum(Stage); /** @internal */ export type EvaluationRequest$Outbound = { - agent_uuid: string; + agent_name: string; stage: string; step: Step$Outbound; }; @@ -63,13 +63,13 @@ export const EvaluationRequest$outboundSchema: z.ZodMiniType< EvaluationRequest > = z.pipe( z.object({ - agentUuid: z.string(), + agentName: z.string(), stage: Stage$outboundSchema, step: Step$outboundSchema, }), z.transform((v) => { return remap$(v, { - agentUuid: "agent_uuid", + agentName: "agent_name", }); }), ); diff --git a/sdks/typescript/src/generated/models/event-query-request.ts b/sdks/typescript/src/generated/models/event-query-request.ts index 5903d8fb..b222b642 100644 --- a/sdks/typescript/src/generated/models/event-query-request.ts +++ b/sdks/typescript/src/generated/models/event-query-request.ts @@ -37,7 +37,7 @@ export type CheckStages = ClosedEnum; * trace_id: Filter by trace ID (get all events for a request) * span_id: Filter by span ID (get all events for a function call) * control_execution_id: Filter by specific event ID - * agent_uuid: Filter by agent UUID + * agent_name: Filter by agent identifier * control_ids: Filter by control IDs * actions: Filter by actions (allow, deny, warn, log) * matched: Filter by matched status @@ -54,9 +54,9 @@ export type EventQueryRequest = { */ actions?: Array | null | undefined; /** - * Filter by agent UUID + * Filter by agent identifier */ - agentUuid?: string | null | undefined; + agentName?: string | null | undefined; /** * Filter by call types */ @@ -120,7 +120,7 @@ export const CheckStages$outboundSchema: z.ZodMiniEnum = z /** @internal */ export type EventQueryRequest$Outbound = { actions?: Array | null | undefined; - agent_uuid?: string | null | undefined; + agent_name?: string | null | undefined; applies_to?: Array | null | undefined; check_stages?: Array | null | undefined; control_execution_id?: string | null | undefined; @@ -141,7 +141,7 @@ export const EventQueryRequest$outboundSchema: z.ZodMiniType< > = z.pipe( z.object({ actions: z.optional(z.nullable(z.array(Actions$outboundSchema))), - agentUuid: z.optional(z.nullable(z.string())), + agentName: z.optional(z.nullable(z.string())), appliesTo: z.optional(z.nullable(z.array(AppliesTo$outboundSchema))), checkStages: z.optional(z.nullable(z.array(CheckStages$outboundSchema))), controlExecutionId: z.optional(z.nullable(z.string())), @@ -160,7 +160,7 @@ export const EventQueryRequest$outboundSchema: z.ZodMiniType< }), z.transform((v) => { return remap$(v, { - agentUuid: "agent_uuid", + agentName: "agent_name", appliesTo: "applies_to", checkStages: "check_stages", controlExecutionId: "control_execution_id", diff --git a/sdks/typescript/src/generated/models/get-agent-response.ts b/sdks/typescript/src/generated/models/get-agent-response.ts index b4fc8069..10eacfb3 100644 --- a/sdks/typescript/src/generated/models/get-agent-response.ts +++ b/sdks/typescript/src/generated/models/get-agent-response.ts @@ -24,7 +24,7 @@ export type GetAgentResponse = { * @remarks * * An agent represents an AI system that can be protected and monitored. - * Each agent has a unique ID and can have multiple steps registered with it. + * Each agent has a unique immutable name and can have multiple steps registered with it. */ agent: Agent; /** diff --git a/sdks/typescript/src/generated/models/init-agent-request.ts b/sdks/typescript/src/generated/models/init-agent-request.ts index 8a930bb0..adb9b049 100644 --- a/sdks/typescript/src/generated/models/init-agent-request.ts +++ b/sdks/typescript/src/generated/models/init-agent-request.ts @@ -27,7 +27,7 @@ export type InitAgentRequest = { * @remarks * * An agent represents an AI system that can be protected and monitored. - * Each agent has a unique ID and can have multiple steps registered with it. + * Each agent has a unique immutable name and can have multiple steps registered with it. */ agent: Agent; /** diff --git a/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-id-policy-delete.ts b/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-id-policy-delete.ts deleted file mode 100644 index 48ec8257..00000000 --- a/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-id-policy-delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest = { - agentId: string; -}; - -/** @internal */ -export type DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$Outbound = { - agent_id: string; -}; - -/** @internal */ -export const DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$outboundSchema: - z.ZodMiniType< - DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$Outbound, - DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest - > = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function deleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequestToJSON( - deleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest: - DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, -): string { - return JSON.stringify( - DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$outboundSchema.parse( - deleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-name-policy-delete.ts b/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-name-policy-delete.ts new file mode 100644 index 00000000..ff2543db --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-name-policy-delete.ts @@ -0,0 +1,42 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest = { + agentName: string; +}; + +/** @internal */ +export type DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$Outbound = + { + agent_name: string; + }; + +/** @internal */ +export const DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$outboundSchema: + z.ZodMiniType< + DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$Outbound, + DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function deleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequestToJSON( + deleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest: + DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, +): string { + return JSON.stringify( + DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$outboundSchema + .parse(deleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-id-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-id-get.ts deleted file mode 100644 index 84f2a0e9..00000000 --- a/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-id-get.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type GetAgentApiV1AgentsAgentIdGetRequest = { - agentId: string; -}; - -/** @internal */ -export type GetAgentApiV1AgentsAgentIdGetRequest$Outbound = { - agent_id: string; -}; - -/** @internal */ -export const GetAgentApiV1AgentsAgentIdGetRequest$outboundSchema: z.ZodMiniType< - GetAgentApiV1AgentsAgentIdGetRequest$Outbound, - GetAgentApiV1AgentsAgentIdGetRequest -> = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), -); - -export function getAgentApiV1AgentsAgentIdGetRequestToJSON( - getAgentApiV1AgentsAgentIdGetRequest: GetAgentApiV1AgentsAgentIdGetRequest, -): string { - return JSON.stringify( - GetAgentApiV1AgentsAgentIdGetRequest$outboundSchema.parse( - getAgentApiV1AgentsAgentIdGetRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-name-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-name-get.ts new file mode 100644 index 00000000..0fa6767a --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-name-get.ts @@ -0,0 +1,42 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type GetAgentApiV1AgentsAgentNameGetRequest = { + agentName: string; +}; + +/** @internal */ +export type GetAgentApiV1AgentsAgentNameGetRequest$Outbound = { + agent_name: string; +}; + +/** @internal */ +export const GetAgentApiV1AgentsAgentNameGetRequest$outboundSchema: + z.ZodMiniType< + GetAgentApiV1AgentsAgentNameGetRequest$Outbound, + GetAgentApiV1AgentsAgentNameGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function getAgentApiV1AgentsAgentNameGetRequestToJSON( + getAgentApiV1AgentsAgentNameGetRequest: + GetAgentApiV1AgentsAgentNameGetRequest, +): string { + return JSON.stringify( + GetAgentApiV1AgentsAgentNameGetRequest$outboundSchema.parse( + getAgentApiV1AgentsAgentNameGetRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-id-evaluators-evaluator-name-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-id-evaluators-evaluator-name-get.ts deleted file mode 100644 index 4412a62a..00000000 --- a/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-id-evaluators-evaluator-name-get.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest = - { - agentId: string; - evaluatorName: string; - }; - -/** @internal */ -export type GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest$Outbound = - { - agent_id: string; - evaluator_name: string; - }; - -/** @internal */ -export const GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest$outboundSchema: - z.ZodMiniType< - GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest$Outbound, - GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest - > = z.pipe( - z.object({ - agentId: z.string(), - evaluatorName: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - evaluatorName: "evaluator_name", - }); - }), - ); - -export function getAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequestToJSON( - getAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest: - GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, -): string { - return JSON.stringify( - GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest$outboundSchema - .parse( - getAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-name-evaluators-evaluator-name-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-name-evaluators-evaluator-name-get.ts new file mode 100644 index 00000000..39eda5c3 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-name-evaluators-evaluator-name-get.ts @@ -0,0 +1,49 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest = + { + agentName: string; + evaluatorName: string; + }; + +/** @internal */ +export type GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest$Outbound = + { + agent_name: string; + evaluator_name: string; + }; + +/** @internal */ +export const GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest$outboundSchema: + z.ZodMiniType< + GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest$Outbound, + GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + evaluatorName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + evaluatorName: "evaluator_name", + }); + }), + ); + +export function getAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequestToJSON( + getAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest: + GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, +): string { + return JSON.stringify( + GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest$outboundSchema + .parse( + getAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-id-policy-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-id-policy-get.ts deleted file mode 100644 index e40885cb..00000000 --- a/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-id-policy-get.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest = { - agentId: string; -}; - -/** @internal */ -export type GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$Outbound = { - agent_id: string; -}; - -/** @internal */ -export const GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$outboundSchema: - z.ZodMiniType< - GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$Outbound, - GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest - > = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function getAgentPolicyApiV1AgentsAgentIdPolicyGetRequestToJSON( - getAgentPolicyApiV1AgentsAgentIdPolicyGetRequest: - GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, -): string { - return JSON.stringify( - GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$outboundSchema.parse( - getAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-name-policy-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-name-policy-get.ts new file mode 100644 index 00000000..7642f568 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-name-policy-get.ts @@ -0,0 +1,42 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest = { + agentName: string; +}; + +/** @internal */ +export type GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$Outbound = { + agent_name: string; +}; + +/** @internal */ +export const GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$outboundSchema: + z.ZodMiniType< + GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$Outbound, + GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function getAgentPolicyApiV1AgentsAgentNamePolicyGetRequestToJSON( + getAgentPolicyApiV1AgentsAgentNamePolicyGetRequest: + GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, +): string { + return JSON.stringify( + GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$outboundSchema.parse( + getAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/get-control-stats-api-v1-observability-stats-controls-control-id-get.ts b/sdks/typescript/src/generated/models/operations/get-control-stats-api-v1-observability-stats-controls-control-id-get.ts index e3e26749..56473a89 100644 --- a/sdks/typescript/src/generated/models/operations/get-control-stats-api-v1-observability-stats-controls-control-id-get.ts +++ b/sdks/typescript/src/generated/models/operations/get-control-stats-api-v1-observability-stats-controls-control-id-get.ts @@ -22,7 +22,7 @@ export type QueryParamTimeRange = ClosedEnum; export type GetControlStatsApiV1ObservabilityStatsControlsControlIdGetRequest = { controlId: number; - agentUuid: string; + agentName: string; timeRange?: QueryParamTimeRange | undefined; includeTimeseries?: boolean | undefined; }; @@ -36,7 +36,7 @@ export const QueryParamTimeRange$outboundSchema: z.ZodMiniEnum< export type GetControlStatsApiV1ObservabilityStatsControlsControlIdGetRequest$Outbound = { control_id: number; - agent_uuid: string; + agent_name: string; time_range: string; include_timeseries: boolean; }; @@ -49,14 +49,14 @@ export const GetControlStatsApiV1ObservabilityStatsControlsControlIdGetRequest$o > = z.pipe( z.object({ controlId: z.int(), - agentUuid: z.string(), + agentName: z.string(), timeRange: z._default(QueryParamTimeRange$outboundSchema, "5m"), includeTimeseries: z._default(z.boolean(), false), }), z.transform((v) => { return remap$(v, { controlId: "control_id", - agentUuid: "agent_uuid", + agentName: "agent_name", timeRange: "time_range", includeTimeseries: "include_timeseries", }); diff --git a/sdks/typescript/src/generated/models/operations/get-stats-api-v1-observability-stats-get.ts b/sdks/typescript/src/generated/models/operations/get-stats-api-v1-observability-stats-get.ts index 030726d7..ec6c6ba3 100644 --- a/sdks/typescript/src/generated/models/operations/get-stats-api-v1-observability-stats-get.ts +++ b/sdks/typescript/src/generated/models/operations/get-stats-api-v1-observability-stats-get.ts @@ -20,7 +20,7 @@ export const TimeRange = { export type TimeRange = ClosedEnum; export type GetStatsApiV1ObservabilityStatsGetRequest = { - agentUuid: string; + agentName: string; timeRange?: TimeRange | undefined; includeTimeseries?: boolean | undefined; }; @@ -32,7 +32,7 @@ export const TimeRange$outboundSchema: z.ZodMiniEnum = z.enum( /** @internal */ export type GetStatsApiV1ObservabilityStatsGetRequest$Outbound = { - agent_uuid: string; + agent_name: string; time_range: string; include_timeseries: boolean; }; @@ -44,13 +44,13 @@ export const GetStatsApiV1ObservabilityStatsGetRequest$outboundSchema: GetStatsApiV1ObservabilityStatsGetRequest > = z.pipe( z.object({ - agentUuid: z.string(), + agentName: z.string(), timeRange: z._default(TimeRange$outboundSchema, "5m"), includeTimeseries: z._default(z.boolean(), false), }), z.transform((v) => { return remap$(v, { - agentUuid: "agent_uuid", + agentName: "agent_name", timeRange: "time_range", includeTimeseries: "include_timeseries", }); diff --git a/sdks/typescript/src/generated/models/operations/index.ts b/sdks/typescript/src/generated/models/operations/index.ts index 2a322b0f..8e6bebc2 100644 --- a/sdks/typescript/src/generated/models/operations/index.ts +++ b/sdks/typescript/src/generated/models/operations/index.ts @@ -3,27 +3,27 @@ */ export * from "./add-control-to-policy-api-v1-policies-policy-id-controls-control-id-post.js"; -export * from "./delete-agent-policy-api-v1-agents-agent-id-policy-delete.js"; +export * from "./delete-agent-policy-api-v1-agents-agent-name-policy-delete.js"; export * from "./delete-control-api-v1-controls-control-id-delete.js"; export * from "./delete-evaluator-config-api-v1-evaluator-configs-config-id-delete.js"; export * from "./evaluate-api-v1-evaluation-post.js"; -export * from "./get-agent-api-v1-agents-agent-id-get.js"; -export * from "./get-agent-evaluator-api-v1-agents-agent-id-evaluators-evaluator-name-get.js"; -export * from "./get-agent-policy-api-v1-agents-agent-id-policy-get.js"; +export * from "./get-agent-api-v1-agents-agent-name-get.js"; +export * from "./get-agent-evaluator-api-v1-agents-agent-name-evaluators-evaluator-name-get.js"; +export * from "./get-agent-policy-api-v1-agents-agent-name-policy-get.js"; export * from "./get-control-api-v1-controls-control-id-get.js"; export * from "./get-control-data-api-v1-controls-control-id-data-get.js"; export * from "./get-control-stats-api-v1-observability-stats-controls-control-id-get.js"; export * from "./get-evaluator-config-api-v1-evaluator-configs-config-id-get.js"; export * from "./get-stats-api-v1-observability-stats-get.js"; -export * from "./list-agent-controls-api-v1-agents-agent-id-controls-get.js"; -export * from "./list-agent-evaluators-api-v1-agents-agent-id-evaluators-get.js"; +export * from "./list-agent-controls-api-v1-agents-agent-name-controls-get.js"; +export * from "./list-agent-evaluators-api-v1-agents-agent-name-evaluators-get.js"; export * from "./list-agents-api-v1-agents-get.js"; export * from "./list-controls-api-v1-controls-get.js"; export * from "./list-evaluator-configs-api-v1-evaluator-configs-get.js"; export * from "./list-policy-controls-api-v1-policies-policy-id-controls-get.js"; -export * from "./patch-agent-api-v1-agents-agent-id-patch.js"; +export * from "./patch-agent-api-v1-agents-agent-name-patch.js"; export * from "./patch-control-api-v1-controls-control-id-patch.js"; export * from "./remove-control-from-policy-api-v1-policies-policy-id-controls-control-id-delete.js"; -export * from "./set-agent-policy-api-v1-agents-agent-id-policy-policy-id-post.js"; +export * from "./set-agent-policy-api-v1-agents-agent-name-policy-policy-id-post.js"; export * from "./set-control-data-api-v1-controls-control-id-data-put.js"; export * from "./update-evaluator-config-api-v1-evaluator-configs-config-id-put.js"; diff --git a/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-id-controls-get.ts b/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-id-controls-get.ts deleted file mode 100644 index 09f5462a..00000000 --- a/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-id-controls-get.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type ListAgentControlsApiV1AgentsAgentIdControlsGetRequest = { - agentId: string; -}; - -/** @internal */ -export type ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$Outbound = { - agent_id: string; -}; - -/** @internal */ -export const ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$outboundSchema: - z.ZodMiniType< - ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$Outbound, - ListAgentControlsApiV1AgentsAgentIdControlsGetRequest - > = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function listAgentControlsApiV1AgentsAgentIdControlsGetRequestToJSON( - listAgentControlsApiV1AgentsAgentIdControlsGetRequest: - ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, -): string { - return JSON.stringify( - ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$outboundSchema.parse( - listAgentControlsApiV1AgentsAgentIdControlsGetRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-name-controls-get.ts b/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-name-controls-get.ts new file mode 100644 index 00000000..ddc50315 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-name-controls-get.ts @@ -0,0 +1,41 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type ListAgentControlsApiV1AgentsAgentNameControlsGetRequest = { + agentName: string; +}; + +/** @internal */ +export type ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$Outbound = { + agent_name: string; +}; + +/** @internal */ +export const ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$outboundSchema: + z.ZodMiniType< + ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$Outbound, + ListAgentControlsApiV1AgentsAgentNameControlsGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function listAgentControlsApiV1AgentsAgentNameControlsGetRequestToJSON( + listAgentControlsApiV1AgentsAgentNameControlsGetRequest: + ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, +): string { + return JSON.stringify( + ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$outboundSchema + .parse(listAgentControlsApiV1AgentsAgentNameControlsGetRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-id-evaluators-get.ts b/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-id-evaluators-get.ts deleted file mode 100644 index 55185ff8..00000000 --- a/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-id-evaluators-get.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest = { - agentId: string; - cursor?: string | null | undefined; - limit?: number | undefined; -}; - -/** @internal */ -export type ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest$Outbound = - { - agent_id: string; - cursor?: string | null | undefined; - limit: number; - }; - -/** @internal */ -export const ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest$outboundSchema: - z.ZodMiniType< - ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest$Outbound, - ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest - > = z.pipe( - z.object({ - agentId: z.string(), - cursor: z.optional(z.nullable(z.string())), - limit: z._default(z.int(), 20), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function listAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequestToJSON( - listAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest: - ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest, -): string { - return JSON.stringify( - ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest$outboundSchema - .parse(listAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-name-evaluators-get.ts b/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-name-evaluators-get.ts new file mode 100644 index 00000000..2aaf3560 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-name-evaluators-get.ts @@ -0,0 +1,48 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest = { + agentName: string; + cursor?: string | null | undefined; + limit?: number | undefined; +}; + +/** @internal */ +export type ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest$Outbound = + { + agent_name: string; + cursor?: string | null | undefined; + limit: number; + }; + +/** @internal */ +export const ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest$outboundSchema: + z.ZodMiniType< + ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest$Outbound, + ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + cursor: z.optional(z.nullable(z.string())), + limit: z._default(z.int(), 20), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function listAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequestToJSON( + listAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest: + ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest, +): string { + return JSON.stringify( + ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest$outboundSchema + .parse(listAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-id-patch.ts b/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-id-patch.ts deleted file mode 100644 index bfd30824..00000000 --- a/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-id-patch.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; -import * as models from "../index.js"; - -export type PatchAgentApiV1AgentsAgentIdPatchRequest = { - agentId: string; - body: models.PatchAgentRequest; -}; - -/** @internal */ -export type PatchAgentApiV1AgentsAgentIdPatchRequest$Outbound = { - agent_id: string; - body: models.PatchAgentRequest$Outbound; -}; - -/** @internal */ -export const PatchAgentApiV1AgentsAgentIdPatchRequest$outboundSchema: - z.ZodMiniType< - PatchAgentApiV1AgentsAgentIdPatchRequest$Outbound, - PatchAgentApiV1AgentsAgentIdPatchRequest - > = z.pipe( - z.object({ - agentId: z.string(), - body: models.PatchAgentRequest$outboundSchema, - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function patchAgentApiV1AgentsAgentIdPatchRequestToJSON( - patchAgentApiV1AgentsAgentIdPatchRequest: - PatchAgentApiV1AgentsAgentIdPatchRequest, -): string { - return JSON.stringify( - PatchAgentApiV1AgentsAgentIdPatchRequest$outboundSchema.parse( - patchAgentApiV1AgentsAgentIdPatchRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-name-patch.ts b/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-name-patch.ts new file mode 100644 index 00000000..5bb9f012 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-name-patch.ts @@ -0,0 +1,46 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; +import * as models from "../index.js"; + +export type PatchAgentApiV1AgentsAgentNamePatchRequest = { + agentName: string; + body: models.PatchAgentRequest; +}; + +/** @internal */ +export type PatchAgentApiV1AgentsAgentNamePatchRequest$Outbound = { + agent_name: string; + body: models.PatchAgentRequest$Outbound; +}; + +/** @internal */ +export const PatchAgentApiV1AgentsAgentNamePatchRequest$outboundSchema: + z.ZodMiniType< + PatchAgentApiV1AgentsAgentNamePatchRequest$Outbound, + PatchAgentApiV1AgentsAgentNamePatchRequest + > = z.pipe( + z.object({ + agentName: z.string(), + body: models.PatchAgentRequest$outboundSchema, + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function patchAgentApiV1AgentsAgentNamePatchRequestToJSON( + patchAgentApiV1AgentsAgentNamePatchRequest: + PatchAgentApiV1AgentsAgentNamePatchRequest, +): string { + return JSON.stringify( + PatchAgentApiV1AgentsAgentNamePatchRequest$outboundSchema.parse( + patchAgentApiV1AgentsAgentNamePatchRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-id-policy-policy-id-post.ts b/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-id-policy-policy-id-post.ts deleted file mode 100644 index aa9b0121..00000000 --- a/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-id-policy-policy-id-post.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { remap as remap$ } from "../../lib/primitives.js"; - -export type SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest = { - agentId: string; - policyId: number; -}; - -/** @internal */ -export type SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$Outbound = - { - agent_id: string; - policy_id: number; - }; - -/** @internal */ -export const SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$outboundSchema: - z.ZodMiniType< - SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$Outbound, - SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest - > = z.pipe( - z.object({ - agentId: z.string(), - policyId: z.int(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - policyId: "policy_id", - }); - }), - ); - -export function setAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequestToJSON( - setAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest: - SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, -): string { - return JSON.stringify( - SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$outboundSchema - .parse(setAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-name-policy-policy-id-post.ts b/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-name-policy-policy-id-post.ts new file mode 100644 index 00000000..87c5bf6a --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-name-policy-policy-id-post.ts @@ -0,0 +1,46 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { remap as remap$ } from "../../lib/primitives.js"; + +export type SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest = { + agentName: string; + policyId: number; +}; + +/** @internal */ +export type SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$Outbound = + { + agent_name: string; + policy_id: number; + }; + +/** @internal */ +export const SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$outboundSchema: + z.ZodMiniType< + SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$Outbound, + SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest + > = z.pipe( + z.object({ + agentName: z.string(), + policyId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + policyId: "policy_id", + }); + }), + ); + +export function setAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequestToJSON( + setAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest: + SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, +): string { + return JSON.stringify( + SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$outboundSchema + .parse(setAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/stats-response.ts b/sdks/typescript/src/generated/models/stats-response.ts index b4c03368..7ac09190 100644 --- a/sdks/typescript/src/generated/models/stats-response.ts +++ b/sdks/typescript/src/generated/models/stats-response.ts @@ -19,16 +19,16 @@ import { StatsTotals, StatsTotals$inboundSchema } from "./stats-totals.js"; * Contains agent-level totals (with optional timeseries) and per-control breakdown. * * Attributes: - * agent_uuid: Agent UUID + * agent_name: Agent identifier * time_range: Time range used * totals: Agent-level aggregate statistics (includes timeseries) * controls: Per-control breakdown for discovery and detail */ export type StatsResponse = { /** - * Agent UUID + * Agent identifier */ - agentUuid: string; + agentName: string; /** * Per-control breakdown */ @@ -64,14 +64,14 @@ export const StatsResponse$inboundSchema: z.ZodMiniType< unknown > = z.pipe( z.object({ - agent_uuid: types.string(), + agent_name: types.string(), controls: z.array(ControlStats$inboundSchema), time_range: types.string(), totals: StatsTotals$inboundSchema, }), z.transform((v) => { return remap$(v, { - "agent_uuid": "agentUuid", + "agent_name": "agentName", "time_range": "timeRange", }); }), diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index 8a15dffb..98c0c874 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -24,11 +24,11 @@ export class Agents extends ClientSDK { * @remarks * List all registered agents with cursor-based pagination. * - * Returns a summary of each agent including ID, name, policy assignment, + * Returns a summary of each agent including identifier, policy assignment, * and counts of registered steps and evaluators. * * Args: - * cursor: Optional cursor for pagination (UUID of last agent from previous page) + * cursor: Optional cursor for pagination (last agent name from previous page) * limit: Pagination limit (default 20, max 100) * name: Optional name filter (case-insensitive partial match) * db: Database session (injected) @@ -55,9 +55,7 @@ export class Agents extends ClientSDK { * * This endpoint is idempotent: * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists with the same UUID, updates registration data - * - If the agent name exists with a different UUID, returns 409 Conflict - * - If the UUID exists with a different name, returns 409 Conflict (no renames) + * - If the agent name exists, updates registration data in place * * conflict_mode controls registration conflict handling: * - strict (default): preserve compatibility checks and conflict errors @@ -69,10 +67,6 @@ export class Agents extends ClientSDK { * * Returns: * InitAgentResponse with created flag and active controls (if policy assigned) - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update */ async init( request: models.InitAgentRequest, @@ -94,7 +88,7 @@ export class Agents extends ClientSDK { * Returns the latest version of each step (deduplicated by type+name). * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -105,7 +99,7 @@ export class Agents extends ClientSDK { * HTTPException 422: Agent data is corrupted */ async get( - request: operations.GetAgentApiV1AgentsAgentIdGetRequest, + request: operations.GetAgentApiV1AgentsAgentNameGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsGet( @@ -125,7 +119,7 @@ export class Agents extends ClientSDK { * Removals are idempotent - attempting to remove non-existent items is not an error. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * request: Lists of step/evaluator identifiers to remove * db: Database session (injected) * @@ -137,7 +131,7 @@ export class Agents extends ClientSDK { * HTTPException 500: Database error during update */ async update( - request: operations.PatchAgentApiV1AgentsAgentIdPatchRequest, + request: operations.PatchAgentApiV1AgentsAgentNamePatchRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsUpdate( @@ -157,7 +151,7 @@ export class Agents extends ClientSDK { * Returns an empty list if the agent has no policy. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -167,7 +161,7 @@ export class Agents extends ClientSDK { * HTTPException 404: Agent not found */ async listControls( - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsListControls( @@ -188,7 +182,7 @@ export class Agents extends ClientSDK { * - UI to display available config options * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * cursor: Optional cursor for pagination (name of last evaluator from previous page) * limit: Pagination limit (default 20, max 100) * db: Database session (injected) @@ -201,7 +195,7 @@ export class Agents extends ClientSDK { */ async listEvaluators( request: - operations.ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest, + operations.ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsListEvaluators( @@ -218,7 +212,7 @@ export class Agents extends ClientSDK { * Get a specific evaluator schema registered with an agent. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * evaluator_name: Name of the evaluator * db: Database session (injected) * @@ -230,7 +224,7 @@ export class Agents extends ClientSDK { */ async getEvaluator( request: - operations.GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, + operations.GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsGetEvaluator( @@ -249,7 +243,7 @@ export class Agents extends ClientSDK { * The agent will no longer have any protection controls active. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -260,7 +254,8 @@ export class Agents extends ClientSDK { * HTTPException 500: Database error during removal */ async deletePolicy( - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + request: + operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsDeletePolicy( @@ -277,7 +272,7 @@ export class Agents extends ClientSDK { * Retrieve the policy currently assigned to an agent. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -287,7 +282,7 @@ export class Agents extends ClientSDK { * HTTPException 404: Agent not found or agent has no policy assigned */ async getPolicy( - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsGetPolicy( @@ -306,7 +301,7 @@ export class Agents extends ClientSDK { * The agent will immediately inherit all controls from the assigned policy. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * policy_id: ID of the policy to assign * db: Database session (injected) * @@ -319,7 +314,7 @@ export class Agents extends ClientSDK { */ async updatePolicy( request: - operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsUpdatePolicy( diff --git a/sdks/typescript/src/generated/sdk/evaluators.ts b/sdks/typescript/src/generated/sdk/evaluators.ts index c78145c0..b5bab688 100644 --- a/sdks/typescript/src/generated/sdk/evaluators.ts +++ b/sdks/typescript/src/generated/sdk/evaluators.ts @@ -23,7 +23,7 @@ export class Evaluators extends ClientSDK { * - **sql**: SQL query validation * * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. */ async list( options?: RequestOptions, diff --git a/sdks/typescript/src/generated/sdk/observability.ts b/sdks/typescript/src/generated/sdk/observability.ts index 6c59fc48..1697b9a8 100644 --- a/sdks/typescript/src/generated/sdk/observability.ts +++ b/sdks/typescript/src/generated/sdk/observability.ts @@ -49,7 +49,7 @@ export class Observability extends ClientSDK { * - trace_id: Get all events for a request * - span_id: Get all events for a function call * - control_execution_id: Get a specific event - * - agent_uuid: Filter by agent + * - agent_name: Filter by agent * - control_ids: Filter by controls * - actions: Filter by actions (allow, deny, warn, log) * - matched: Filter by matched status @@ -87,7 +87,7 @@ export class Observability extends ClientSDK { * Use /stats/controls/{control_id} for single control stats. * * Args: - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) @@ -116,7 +116,7 @@ export class Observability extends ClientSDK { * * Args: * control_id: Control ID to get stats for - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) diff --git a/sdks/typescript/tests/client-api.test.ts b/sdks/typescript/tests/client-api.test.ts index c6c11864..40899b46 100644 --- a/sdks/typescript/tests/client-api.test.ts +++ b/sdks/typescript/tests/client-api.test.ts @@ -106,7 +106,6 @@ describe("AgentControlClient API wiring", () => { await client.agents.init({ agent: { - agentId: "550e8400-e29b-41d4-a716-446655440000", agentName: "test-agent", }, }); diff --git a/sdks/typescript/tests/test_generate_method_names_overlay.py b/sdks/typescript/tests/test_generate_method_names_overlay.py index 311d6e80..c36db98e 100644 --- a/sdks/typescript/tests/test_generate_method_names_overlay.py +++ b/sdks/typescript/tests/test_generate_method_names_overlay.py @@ -66,9 +66,9 @@ def test_plural_collection_get_is_list_but_singular_get_is_get(overlay_gen): "operationId": "list_agents_api_v1_agents_get", }, }, - "/api/v1/agents/{agent_id}": { + "/api/v1/agents/{agent_name}": { "get": { - "operationId": "get_agent_api_v1_agents__agent_id__get", + "operationId": "get_agent_api_v1_agents__agent_name__get", }, }, "/health": { @@ -83,7 +83,7 @@ def test_plural_collection_get_is_list_but_singular_get_is_get(overlay_gen): names = overlay_gen.resolve_names(operations) assert names[("/api/v1/agents", "get")] == ("agents", "list") - assert names[("/api/v1/agents/{agent_id}", "get")] == ("agents", "get") + assert names[("/api/v1/agents/{agent_name}", "get")] == ("agents", "get") assert names[("/health", "get")] == ("system", "healthCheck") diff --git a/server/README.md b/server/README.md index 7af8ec01..57e64f26 100644 --- a/server/README.md +++ b/server/README.md @@ -159,10 +159,10 @@ POST /api/v1/agents/init Body: { "agent": {...}, "tools": [...], "force_replace": false } # Get agent -GET /api/v1/agents/{agent_id} +GET /api/v1/agents/{agent_name} # List controls for agent (based on assigned policy) -GET /api/v1/agents/{agent_id}/controls +GET /api/v1/agents/{agent_name}/controls ``` ### Control Management @@ -197,7 +197,7 @@ Body: { "name": "my-policy", "description": "..." } GET /api/v1/policies # Assign policy to agent -POST /api/v1/policies/{policy_id}/agents/{agent_id} +POST /api/v1/policies/{policy_id}/agents/{agent_name} # Add control to policy POST /api/v1/policies/{policy_id}/controls/{control_id} @@ -209,7 +209,7 @@ POST /api/v1/policies/{policy_id}/controls/{control_id} # Evaluate step against controls POST /api/v1/evaluation Body: { - "agent_uuid": "uuid", + "agent_name": "uuid", "step": { "type": "llm", "name": "chat", "input": "..." }, "stage": "pre" } @@ -231,13 +231,13 @@ Body: { "events": [...] } # Query events POST /api/v1/observability/events/query -Body: { "agent_uuid": "...", "start_time": "...", ... } +Body: { "agent_name": "...", "start_time": "...", ... } # Get agent stats -GET /api/v1/observability/stats?agent_uuid=...&time_range=5m +GET /api/v1/observability/stats?agent_name=...&time_range=5m # Get control stats -GET /api/v1/observability/stats/controls/{control_id}?agent_uuid=...&time_range=5m +GET /api/v1/observability/stats/controls/{control_id}?agent_name=...&time_range=5m ``` See [docs/REFERENCE.md](../docs/REFERENCE.md) for complete API documentation. diff --git a/server/alembic/versions/58920e6807fe_name_native_agent_identity_hard_cut.py b/server/alembic/versions/58920e6807fe_name_native_agent_identity_hard_cut.py new file mode 100644 index 00000000..26952e4c --- /dev/null +++ b/server/alembic/versions/58920e6807fe_name_native_agent_identity_hard_cut.py @@ -0,0 +1,52 @@ +""" +Revision ID: 58920e6807fe +Revises: d2f4a6b8c9d0 +Create Date: 2026-02-25 17:56:34.057000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '58920e6807fe' +down_revision = 'd2f4a6b8c9d0' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(op.f('agents_name_key'), 'agents', type_='unique') + op.drop_column('agents', 'agent_uuid') + op.create_primary_key('agents_pkey', 'agents', ['name']) + op.create_check_constraint('ck_agents_name_min_length', 'agents', 'char_length(name) >= 10') + op.create_check_constraint('ck_agents_name_format', 'agents', "name ~ '^[a-z0-9:_-]+$'") + op.add_column('control_execution_events', sa.Column('agent_name', sa.String(length=255), nullable=False)) + op.drop_index(op.f('ix_events_agent_time'), table_name='control_execution_events') + op.create_index('ix_events_agent_time', 'control_execution_events', ['agent_name', sa.literal_column('timestamp DESC')], unique=False) + op.execute(""" + CREATE INDEX IF NOT EXISTS ix_events_data_control_id + ON control_execution_events ((data ->> 'control_id'::text)) + """) + op.drop_column('control_execution_events', 'agent_uuid') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('control_execution_events', sa.Column('agent_uuid', sa.UUID(), autoincrement=False, nullable=False)) + op.drop_index('ix_events_agent_time', table_name='control_execution_events') + op.create_index(op.f('ix_events_agent_time'), 'control_execution_events', ['agent_uuid', sa.literal_column('timestamp DESC')], unique=False) + op.drop_column('control_execution_events', 'agent_name') + op.drop_constraint('ck_agents_name_format', 'agents', type_='check') + op.drop_constraint('ck_agents_name_min_length', 'agents', type_='check') + op.drop_constraint('agents_pkey', 'agents', type_='primary') + op.add_column('agents', sa.Column('agent_uuid', sa.UUID(), autoincrement=False, nullable=False)) + op.create_primary_key('agents_pkey', 'agents', ['agent_uuid']) + op.create_unique_constraint(op.f('agents_name_key'), 'agents', ['name'], postgresql_nulls_not_distinct=False) + op.execute(""" + CREATE INDEX IF NOT EXISTS ix_events_data_control_id + ON control_execution_events ((data ->> 'control_id'::text)) + """) + # ### end Alembic commands ### diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 7e15ef88..82cecfc9 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -1,5 +1,4 @@ from typing import Any -from uuid import UUID from agent_control_engine import list_evaluators from agent_control_models.agent import Agent as APIAgent @@ -46,6 +45,7 @@ Policy, policy_controls, ) +from ..services.agent_names import normalize_agent_name_or_422 from ..services.controls import list_controls_for_agent, list_controls_for_policy from ..services.evaluator_utils import ( parse_evaluator_ref_full, @@ -182,7 +182,7 @@ async def _build_overwrite_evaluator_removals( try: controls = await list_controls_for_agent( - agent.agent_uuid, + agent.name, db, allow_invalid_step_name_regex=True, ) @@ -239,11 +239,11 @@ async def list_agents( """ List all registered agents with cursor-based pagination. - Returns a summary of each agent including ID, name, policy assignment, + Returns a summary of each agent including identifier, policy assignment, and counts of registered steps and evaluators. Args: - cursor: Optional cursor for pagination (UUID of last agent from previous page) + cursor: Optional cursor for pagination (last agent name from previous page) limit: Pagination limit (default 20, max 100) name: Optional name filter (case-insensitive partial match) db: Database session (injected) @@ -265,8 +265,8 @@ async def list_agents( total = count_result.scalar() or 0 # Build query with cursor-based pagination - # Order by created_at DESC, then by UUID DESC for stable ordering - query = select(Agent).order_by(Agent.created_at.desc(), Agent.agent_uuid.desc()) + # Order by created_at DESC, then by name DESC for stable ordering + query = select(Agent).order_by(Agent.created_at.desc(), Agent.name.desc()) # Apply name filter if provided if name_filter is not None: @@ -274,25 +274,19 @@ async def list_agents( # If cursor provided, filter to get items after the cursor if cursor: - try: - cursor_uuid = UUID(cursor) - # Get the cursor agent to find its created_at timestamp - cursor_agent_result = await db.execute( - select(Agent).where(Agent.agent_uuid == cursor_uuid) - ) - cursor_agent = cursor_agent_result.scalars().first() - if cursor_agent: - # Get agents created before this one (or same timestamp but smaller UUID) - query = query.where( - (Agent.created_at < cursor_agent.created_at) - | ( - (Agent.created_at == cursor_agent.created_at) - & (Agent.agent_uuid < cursor_agent.agent_uuid) - ) + cursor_name = normalize_agent_name_or_422(cursor, field_name="cursor") + cursor_agent_result = await db.execute( + select(Agent).where(Agent.name == cursor_name) + ) + cursor_agent = cursor_agent_result.scalars().first() + if cursor_agent: + query = query.where( + (Agent.created_at < cursor_agent.created_at) + | ( + (Agent.created_at == cursor_agent.created_at) + & (Agent.name < cursor_agent.name) ) - except ValueError: - # Invalid cursor UUID, ignore it and return first page - pass + ) # Fetch limit + 1 to check if there are more pages query = query.limit(limit + 1) @@ -304,28 +298,28 @@ async def list_agents( if has_more: agents = agents[:-1] # Remove the extra item - # Determine next cursor (UUID of last agent in this page) + # Determine next cursor (name of last agent in this page) next_cursor: str | None = None if has_more and agents: - next_cursor = str(agents[-1].agent_uuid) + next_cursor = agents[-1].name # Batch query: Get control counts for all agents at once # Join: Agent -> Policy -> policy_controls (junction table) -> Control # Count distinct enabled control IDs per agent # Performance: Filter NULL controls explicitly to avoid JSONB parsing on NULL rows # This allows the query planner to optimize better - control_counts_map: dict[UUID, int] = {} + control_counts_map: dict[str, int] = {} if agents: control_counts_query = ( select( - Agent.agent_uuid, + Agent.name, func.count(func.distinct(policy_controls.c.control_id)).label("count"), ) .outerjoin(Policy, Agent.policy_id == Policy.id) .outerjoin(policy_controls, Policy.id == policy_controls.c.policy_id) .outerjoin(Control, policy_controls.c.control_id == Control.id) .where( - Agent.agent_uuid.in_([agent.agent_uuid for agent in agents]), + Agent.name.in_([agent.name for agent in agents]), # Only count enabled controls: Control must exist AND be enabled # (enabled=true OR enabled key missing, default is True) Control.id.is_not(None), # Exclude NULL controls (agents without policies) @@ -334,7 +328,7 @@ async def list_agents( ~Control.data.has_key("enabled"), ), ) - .group_by(Agent.agent_uuid) + .group_by(Agent.name) ) control_counts_result = await db.execute(control_counts_query) control_counts_map = {row[0]: row[1] for row in control_counts_result.all()} @@ -355,11 +349,10 @@ async def list_agents( _logger.warning("Agent '%s' has invalid data, using zero counts", agent.name) # Get active controls count from batched query result - active_controls = control_counts_map.get(agent.agent_uuid, 0) + active_controls = control_counts_map.get(agent.name, 0) summaries.append( AgentSummary( - agent_id=str(agent.agent_uuid), agent_name=agent.name, policy_id=agent.policy_id, created_at=agent.created_at.isoformat() if agent.created_at else None, @@ -394,9 +387,7 @@ async def init_agent( This endpoint is idempotent: - If the agent name doesn't exist, creates a new agent - - If the agent name exists with the same UUID, updates registration data - - If the agent name exists with a different UUID, returns 409 Conflict - - If the UUID exists with a different name, returns 409 Conflict (no renames) + - If the agent name exists, updates registration data in place conflict_mode controls registration conflict handling: - strict (default): preserve compatibility checks and conflict errors @@ -408,10 +399,6 @@ async def init_agent( Returns: InitAgentResponse with created flag and active controls (if policy assigned) - - Raises: - HTTPException 409: Agent name exists with different UUID - HTTPException 500: Database error during creation/update """ # Check for evaluator name collisions with built-in evaluators builtin_names = _get_builtin_evaluator_names() @@ -457,42 +444,8 @@ async def init_agent( ) incoming_steps_by_key[step_key] = step - # Look up by UUID first (primary key) - result = await db.execute(select(Agent).where(Agent.agent_uuid == request.agent.agent_id)) - existing_by_uuid: Agent | None = result.scalars().first() - - # Perf optimization: If UUID exists, skip name query on hot path - if existing_by_uuid is not None: - # Validate name hasn't changed (no rename via initAgent) - if existing_by_uuid.name != request.agent.agent_name: - raise ConflictError( - error_code=ErrorCode.AGENT_NAME_CONFLICT, - detail=( - f"Agent ID '{request.agent.agent_id}' is already registered " - f"with name '{existing_by_uuid.name}'" - ), - resource="Agent", - resource_id=str(request.agent.agent_id), - hint="Use the existing agent name for this UUID or register a new UUID.", - errors=[ - ValidationErrorItem( - resource="Agent", - field="agent_name", - code="name_mismatch", - message=( - f"Agent ID '{request.agent.agent_id}' is already associated " - f"with name '{existing_by_uuid.name}'" - ), - value=request.agent.agent_name, - ) - ], - ) - existing: Agent | None = existing_by_uuid - else: - # UUID doesn't exist, check if name is taken by different agent - result = await db.execute(select(Agent).where(Agent.name == request.agent.agent_name)) - existing_by_name: Agent | None = result.scalars().first() - existing = existing_by_name + result = await db.execute(select(Agent).where(Agent.name == request.agent.agent_name)) + existing: Agent | None = result.scalars().first() created = False @@ -507,7 +460,6 @@ async def init_agent( new_agent = Agent( name=request.agent.agent_name, - agent_uuid=request.agent.agent_id, data=data_model.model_dump(mode="json"), ) db.add(new_agent) @@ -520,7 +472,7 @@ async def init_agent( except Exception: await db.rollback() _logger.error( - f"Failed to create agent '{request.agent.agent_name}' ({request.agent.agent_id})", + f"Failed to create agent '{request.agent.agent_name}' ({request.agent.agent_name})", exc_info=True, ) raise DatabaseError( @@ -530,26 +482,6 @@ async def init_agent( ) return InitAgentResponse(created=created, controls=[]) - requested_uuid = request.agent.agent_id - if existing.agent_uuid != requested_uuid: - # UUID mismatch for the same name: return error - raise ConflictError( - error_code=ErrorCode.AGENT_UUID_CONFLICT, - detail=f"Agent name '{request.agent.agent_name}' already exists with different UUID", - resource="Agent", - resource_id=request.agent.agent_name, - hint="Use the existing agent's UUID or choose a different agent name.", - errors=[ - ValidationErrorItem( - resource="Agent", - field="agent_id", - code="uuid_mismatch", - message=f"Agent '{request.agent.agent_name}' exists with a different UUID", - value=str(requested_uuid), - ) - ], - ) - # Parse existing data via AgentData Pydantic model try: data_model = AgentData.model_validate(existing.data) @@ -788,7 +720,7 @@ async def init_agent( except Exception: await db.rollback() _logger.error( - f"Failed to update agent '{request.agent.agent_name}' ({request.agent.agent_id})", + f"Failed to update agent '{request.agent.agent_name}' ({request.agent.agent_name})", exc_info=True, ) raise DatabaseError( @@ -800,7 +732,7 @@ async def init_agent( # If the existing agent has a policy, include its controls; otherwise empty list controls = [] if existing.policy_id is not None: - controls = await list_controls_for_agent(existing.agent_uuid, db) + controls = await list_controls_for_agent(existing.name, db) return InitAgentResponse( created=created, @@ -811,19 +743,19 @@ async def init_agent( @router.get( - "/{agent_id}", + "/{agent_name}", response_model=GetAgentResponse, summary="Get agent details", response_description="Agent metadata and registered steps", ) -async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> GetAgentResponse: +async def get_agent(agent_name: str, db: AsyncSession = Depends(get_async_db)) -> GetAgentResponse: """ Retrieve agent metadata and all registered steps. Returns the latest version of each step (deduplicated by type+name). Args: - agent_id: UUID of the agent + agent_name: Agent identifier db: Database session (injected) Returns: @@ -833,22 +765,26 @@ async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> HTTPException 404: Agent not found HTTPException 422: Agent data is corrupted """ - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + agent_name = normalize_agent_name_or_422(agent_name) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) existing: Agent | None = result.scalars().first() if existing is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered via initAgent.", + resource_id=str(agent_name), + hint=( + "Verify the agent name is correct and the agent has been " + "registered via initAgent." + ), ) try: data_model = AgentData.model_validate(existing.data) except ValidationError: _logger.error( - f"Failed to parse agent data for agent '{existing.name}' ({agent_id})", + f"Failed to parse agent data for agent '{existing.name}' ({agent_name})", exc_info=True, ) raise APIValidationError( @@ -867,7 +803,7 @@ async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> agent_meta = APIAgent.model_validate(data_model.agent_metadata) except ValidationError: _logger.error( - f"Failed to parse agent metadata for agent '{existing.name}' ({agent_id})", + f"Failed to parse agent metadata for agent '{existing.name}' ({agent_name})", exc_info=True, ) raise APIValidationError( @@ -883,13 +819,13 @@ async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> @router.post( - "/{agent_id}/policy/{policy_id}", + "/{agent_name}/policy/{policy_id}", response_model=SetPolicyResponse, summary="Assign policy to agent", response_description="Success status with previous policy ID", ) async def set_agent_policy( - agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) + agent_name: str, policy_id: int, db: AsyncSession = Depends(get_async_db) ) -> SetPolicyResponse: """ Assign a policy to an agent, replacing any existing policy assignment. @@ -897,7 +833,7 @@ async def set_agent_policy( The agent will immediately inherit all controls from the assigned policy. Args: - agent_id: UUID of the agent + agent_name: Agent identifier policy_id: ID of the policy to assign db: Database session (injected) @@ -908,16 +844,17 @@ async def set_agent_policy( HTTPException 404: Agent or policy not found HTTPException 500: Database error during assignment """ + agent_name = normalize_agent_name_or_422(agent_name) # Find agent - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) # Find policy by id @@ -962,7 +899,7 @@ async def set_agent_policy( except Exception: await db.rollback() _logger.error( - f"Failed to assign policy '{policy_id}' to agent '{agent.name}' ({agent_id})", + f"Failed to assign policy '{policy_id}' to agent '{agent.name}' ({agent_name})", exc_info=True, ) raise DatabaseError( @@ -975,19 +912,19 @@ async def set_agent_policy( @router.get( - "/{agent_id}/policy", + "/{agent_name}/policy", response_model=GetPolicyResponse, summary="Get agent's assigned policy", response_description="Policy ID", ) async def get_agent_policy( - agent_id: UUID, db: AsyncSession = Depends(get_async_db) + agent_name: str, db: AsyncSession = Depends(get_async_db) ) -> GetPolicyResponse: """ Retrieve the policy currently assigned to an agent. Args: - agent_id: UUID of the agent + agent_name: Agent identifier db: Database session (injected) Returns: @@ -996,16 +933,17 @@ async def get_agent_policy( Raises: HTTPException 404: Agent not found or agent has no policy assigned """ + agent_name = normalize_agent_name_or_422(agent_name) # Find agent - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) # Check if agent has a policy @@ -1014,7 +952,7 @@ async def get_agent_policy( error_code=ErrorCode.POLICY_NOT_FOUND, detail=f"Agent '{agent.name}' has no policy assigned", resource="Policy", - hint="Assign a policy to the agent using POST /{agent_id}/policy/{policy_id}.", + hint="Assign a policy to the agent using POST /{agent_name}/policy/{policy_id}.", ) # Find policy @@ -1036,13 +974,13 @@ async def get_agent_policy( @router.delete( - "/{agent_id}/policy", + "/{agent_name}/policy", response_model=DeletePolicyResponse, summary="Remove agent's policy assignment", response_description="Success confirmation", ) async def delete_agent_policy( - agent_id: UUID, db: AsyncSession = Depends(get_async_db) + agent_name: str, db: AsyncSession = Depends(get_async_db) ) -> DeletePolicyResponse: """ Remove the policy assignment from an agent. @@ -1050,7 +988,7 @@ async def delete_agent_policy( The agent will no longer have any protection controls active. Args: - agent_id: UUID of the agent + agent_name: Agent identifier db: Database session (injected) Returns: @@ -1060,16 +998,17 @@ async def delete_agent_policy( HTTPException 404: Agent not found or agent has no policy assigned HTTPException 500: Database error during removal """ + agent_name = normalize_agent_name_or_422(agent_name) # Find agent - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) # Check if agent has a policy @@ -1088,7 +1027,7 @@ async def delete_agent_policy( except Exception: await db.rollback() _logger.error( - f"Failed to remove policy from agent '{agent.name}' ({agent_id})", + f"Failed to remove policy from agent '{agent.name}' ({agent_name})", exc_info=True, ) raise DatabaseError( @@ -1101,13 +1040,13 @@ async def delete_agent_policy( @router.get( - "/{agent_id}/controls", + "/{agent_name}/controls", response_model=AgentControlsResponse, summary="List agent's active controls", response_description="List of controls from agent's policy", ) async def list_agent_controls( - agent_id: UUID, db: AsyncSession = Depends(get_async_db) + agent_name: str, db: AsyncSession = Depends(get_async_db) ) -> AgentControlsResponse: """ List all protection controls active for an agent. @@ -1116,7 +1055,7 @@ async def list_agent_controls( Returns an empty list if the agent has no policy. Args: - agent_id: UUID of the agent + agent_name: Agent identifier db: Database session (injected) Returns: @@ -1125,21 +1064,22 @@ async def list_agent_controls( Raises: HTTPException 404: Agent not found """ - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + agent_name = normalize_agent_name_or_422(agent_name) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) if agent.policy_id is None: return AgentControlsResponse(controls=[]) - controls = await list_controls_for_agent(agent_id, db) + controls = await list_controls_for_agent(agent_name, db) return AgentControlsResponse(controls=controls) @@ -1166,13 +1106,13 @@ class ListEvaluatorsResponse(BaseModel): @router.get( - "/{agent_id}/evaluators", + "/{agent_name}/evaluators", response_model=ListEvaluatorsResponse, summary="List agent's registered evaluator schemas", response_description="Evaluator schemas registered with this agent", ) async def list_agent_evaluators( - agent_id: UUID, + agent_name: str, cursor: str | None = None, limit: int = _DEFAULT_PAGINATION_LIMIT, db: AsyncSession = Depends(get_async_db), @@ -1185,7 +1125,7 @@ async def list_agent_evaluators( - UI to display available config options Args: - agent_id: UUID of the agent + agent_name: Agent identifier cursor: Optional cursor for pagination (name of last evaluator from previous page) limit: Pagination limit (default 20, max 100) db: Database session (injected) @@ -1196,18 +1136,19 @@ async def list_agent_evaluators( Raises: HTTPException 404: Agent not found """ + agent_name = normalize_agent_name_or_422(agent_name) # Clamp limit limit = min(max(1, limit), _MAX_PAGINATION_LIMIT) - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) try: @@ -1261,13 +1202,13 @@ async def list_agent_evaluators( @router.get( - "/{agent_id}/evaluators/{evaluator_name}", + "/{agent_name}/evaluators/{evaluator_name}", response_model=EvaluatorSchemaItem, summary="Get specific evaluator schema", response_description="Evaluator schema details", ) async def get_agent_evaluator( - agent_id: UUID, + agent_name: str, evaluator_name: str, db: AsyncSession = Depends(get_async_db), ) -> EvaluatorSchemaItem: @@ -1275,7 +1216,7 @@ async def get_agent_evaluator( Get a specific evaluator schema registered with an agent. Args: - agent_id: UUID of the agent + agent_name: Agent identifier evaluator_name: Name of the evaluator db: Database session (injected) @@ -1285,15 +1226,16 @@ async def get_agent_evaluator( Raises: HTTPException 404: Agent or evaluator not found """ - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + agent_name = normalize_agent_name_or_422(agent_name) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) try: @@ -1325,13 +1267,13 @@ async def get_agent_evaluator( @router.patch( - "/{agent_id}", + "/{agent_name}", response_model=PatchAgentResponse, summary="Modify agent (remove steps/evaluators)", response_description="Lists of removed items", ) async def patch_agent( - agent_id: UUID, + agent_name: str, request: PatchAgentRequest, db: AsyncSession = Depends(get_async_db), ) -> PatchAgentResponse: @@ -1342,7 +1284,7 @@ async def patch_agent( Removals are idempotent - attempting to remove non-existent items is not an error. Args: - agent_id: UUID of the agent + agent_name: Agent identifier request: Lists of step/evaluator identifiers to remove db: Database session (injected) @@ -1353,15 +1295,16 @@ async def patch_agent( HTTPException 404: Agent not found HTTPException 500: Database error during update """ - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) + agent_name = normalize_agent_name_or_422(agent_name) + result = await db.execute(select(Agent).where(Agent.name == agent_name)) agent: Agent | None = result.scalars().first() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) try: @@ -1398,7 +1341,7 @@ async def patch_agent( # Check if any controls reference evaluators being removed if agent.policy_id is not None: # Get all controls for this agent's policy - controls = await list_controls_for_agent(agent.agent_uuid, db) + controls = await list_controls_for_agent(agent.name, db) referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) for ctrl in controls: @@ -1447,7 +1390,7 @@ async def patch_agent( except Exception: await db.rollback() _logger.error( - f"Failed to patch agent '{agent.name}' ({agent_id})", + f"Failed to patch agent '{agent.name}' ({agent_name})", exc_info=True, ) raise DatabaseError( diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index e4977120..97a85f17 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -601,7 +601,6 @@ async def list_controls( agents_query = ( select( policy_controls.c.control_id, - Agent.agent_uuid, Agent.name, ) .select_from(policy_controls) @@ -611,12 +610,10 @@ async def list_controls( ) agents_result = await db.execute(agents_query) for row in agents_result.all(): - control_id, agent_uuid, agent_name = row + control_id, agent_name = row # Take the first agent found (1 control = 1 agent) if control_agent_map[control_id] is None: - control_agent_map[control_id] = AgentRef( - agent_id=str(agent_uuid), agent_name=agent_name - ) + control_agent_map[control_id] = AgentRef(agent_name=agent_name) # Build summaries (filtering already done at DB level) summaries: list[ControlSummary] = [] diff --git a/server/src/agent_control_server/endpoints/evaluation.py b/server/src/agent_control_server/endpoints/evaluation.py index 6938bdae..34c28c5d 100644 --- a/server/src/agent_control_server/endpoints/evaluation.py +++ b/server/src/agent_control_server/endpoints/evaluation.py @@ -141,22 +141,22 @@ async def evaluate( # Fetch agent to get the name agent_result = await db.execute( - select(Agent).where(Agent.agent_uuid == request.agent_uuid) + select(Agent).where(Agent.name == request.agent_name) ) agent = agent_result.scalar_one_or_none() if agent is None: raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent '{request.agent_uuid}' not found", + detail=f"Agent '{request.agent_name}' not found", resource="Agent", - resource_id=str(request.agent_uuid), + resource_id=request.agent_name, hint="Register the agent via initAgent before evaluating.", ) agent_name = agent.name # Fetch controls for the agent (already validated as ControlDefinition) api_controls = await list_controls_for_agent( - request.agent_uuid, + request.agent_name, db, allow_invalid_step_name_regex=True, ) @@ -244,7 +244,6 @@ async def _emit_observability_events( control_execution_id=match.control_execution_id, trace_id=trace_id, span_id=span_id, - agent_uuid=request.agent_uuid, agent_name=agent_name, control_id=match.control_id, control_name=match.control_name, @@ -270,7 +269,6 @@ async def _emit_observability_events( control_execution_id=error.control_execution_id, trace_id=trace_id, span_id=span_id, - agent_uuid=request.agent_uuid, agent_name=agent_name, control_id=error.control_id, control_name=error.control_name, @@ -296,7 +294,6 @@ async def _emit_observability_events( control_execution_id=non_match.control_execution_id, trace_id=trace_id, span_id=span_id, - agent_uuid=request.agent_uuid, agent_name=agent_name, control_id=non_match.control_id, control_name=non_match.control_name, diff --git a/server/src/agent_control_server/endpoints/evaluators.py b/server/src/agent_control_server/endpoints/evaluators.py index 99f7be27..a9cdaa2a 100644 --- a/server/src/agent_control_server/endpoints/evaluators.py +++ b/server/src/agent_control_server/endpoints/evaluators.py @@ -38,7 +38,7 @@ async def get_evaluators() -> dict[str, EvaluatorInfo]: - **sql**: SQL query validation Custom evaluators are registered per-agent via initAgent. - Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. """ evaluators = list_evaluators() diff --git a/server/src/agent_control_server/endpoints/observability.py b/server/src/agent_control_server/endpoints/observability.py index 61f7bd4c..ea127fc3 100644 --- a/server/src/agent_control_server/endpoints/observability.py +++ b/server/src/agent_control_server/endpoints/observability.py @@ -15,7 +15,6 @@ import logging import time from typing import Literal, cast -from uuid import UUID from agent_control_models import ( BatchEventsRequest, @@ -36,6 +35,7 @@ get_bucket_size, parse_time_range, ) +from ..services.agent_names import normalize_agent_name_or_422 logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ async def query_events( - trace_id: Get all events for a request - span_id: Get all events for a function call - control_execution_id: Get a specific event - - agent_uuid: Filter by agent + - agent_name: Filter by agent - control_ids: Filter by controls - actions: Filter by actions (allow, deny, warn, log) - matched: Filter by matched status @@ -160,7 +160,7 @@ async def query_events( @router.get("/stats", response_model=StatsResponse) async def get_stats( - agent_uuid: UUID, + agent_name: str, time_range: TimeRange = "5m", include_timeseries: bool = False, store: EventStore = Depends(get_event_store), @@ -172,7 +172,7 @@ async def get_stats( Use /stats/controls/{control_id} for single control stats. Args: - agent_uuid: Agent to get stats for + agent_name: Agent to get stats for time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) include_timeseries: Include time-series data points for trend visualization store: Event store (injected) @@ -180,11 +180,12 @@ async def get_stats( Returns: StatsResponse with agent-level totals and per-control breakdown """ + agent_name = normalize_agent_name_or_422(agent_name) interval = parse_time_range(time_range) bucket_size = get_bucket_size(time_range) if include_timeseries else None result = await store.query_stats( - agent_uuid, + agent_name, interval, control_id=None, include_timeseries=include_timeseries, @@ -192,7 +193,7 @@ async def get_stats( ) return StatsResponse( - agent_uuid=agent_uuid, + agent_name=agent_name, time_range=time_range, totals=StatsTotals( execution_count=result.total_executions, @@ -209,7 +210,7 @@ async def get_stats( @router.get("/stats/controls/{control_id}", response_model=ControlStatsResponse) async def get_control_stats( control_id: int, - agent_uuid: UUID, + agent_name: str, time_range: TimeRange = "5m", include_timeseries: bool = False, store: EventStore = Depends(get_event_store), @@ -221,7 +222,7 @@ async def get_control_stats( Args: control_id: Control ID to get stats for - agent_uuid: Agent to get stats for + agent_name: Agent to get stats for time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) include_timeseries: Include time-series data points for trend visualization store: Event store (injected) @@ -229,11 +230,12 @@ async def get_control_stats( Returns: ControlStatsResponse with control stats and optional timeseries """ + agent_name = normalize_agent_name_or_422(agent_name) interval = parse_time_range(time_range) bucket_size = get_bucket_size(time_range) if include_timeseries else None result = await store.query_stats( - agent_uuid, + agent_name, interval, control_id=control_id, include_timeseries=include_timeseries, @@ -244,7 +246,7 @@ async def get_control_stats( control_name = result.stats[0].control_name if result.stats else f"control-{control_id}" return ControlStatsResponse( - agent_uuid=agent_uuid, + agent_name=agent_name, time_range=time_range, control_id=control_id, control_name=control_name, diff --git a/server/src/agent_control_server/errors.py b/server/src/agent_control_server/errors.py index 32d48547..94af5890 100644 --- a/server/src/agent_control_server/errors.py +++ b/server/src/agent_control_server/errors.py @@ -10,9 +10,9 @@ # Raise a not found error raise NotFoundError( error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - resource_id=str(agent_id), + resource_id=agent_name, ) # Raise a validation error with field-level details diff --git a/server/src/agent_control_server/main.py b/server/src/agent_control_server/main.py index e40d171e..3be55738 100644 --- a/server/src/agent_control_server/main.py +++ b/server/src/agent_control_server/main.py @@ -136,7 +136,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 2. Create controls with `/api/v1/controls` and configure them 3. Create a policy and add controls to it 4. Assign the policy to your agent -5. Query agent's active controls with `/api/v1/agents/{agent_id}/controls` +5. Query agent's active controls with `/api/v1/agents/{agent_name}/controls` """, version="0.1.0", lifespan=lifespan, diff --git a/server/src/agent_control_server/models.py b/server/src/agent_control_server/models.py index 093a3b17..383c7681 100644 --- a/server/src/agent_control_server/models.py +++ b/server/src/agent_control_server/models.py @@ -1,12 +1,12 @@ import datetime as dt -import uuid as _uuid from typing import Any, Optional -from agent_control_models.agent import StepSchema +from agent_control_models.agent import StepSchema, normalize_agent_name from agent_control_models.base import BaseModel from agent_control_models.server import EvaluatorSchema from pydantic import Field from sqlalchemy import ( + CheckConstraint, Column, DateTime, ForeignKey, @@ -17,8 +17,7 @@ text, ) from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.dialects.postgresql import UUID as PG_UUID -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from .db import Base @@ -89,11 +88,12 @@ class EvaluatorConfigDB(Base): class Agent(Base): __tablename__ = "agents" - - agent_uuid: Mapped[_uuid.UUID] = mapped_column( - PG_UUID(as_uuid=True), primary_key=True + __table_args__ = ( + CheckConstraint("char_length(name) >= 10", name="ck_agents_name_min_length"), + CheckConstraint("name ~ '^[a-z0-9:_-]+$'", name="ck_agents_name_format"), ) - name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) + + name: Mapped[str] = mapped_column(String(255), primary_key=True) data: Mapped[dict[str, Any]] = mapped_column( JSONB, server_default=text("'{}'::jsonb"), nullable=False ) @@ -105,6 +105,10 @@ class Agent(Base): DateTime(), server_default=text("CURRENT_TIMESTAMP"), nullable=False, index=True ) + @validates("name") + def _normalize_name(self, _key: str, value: str) -> str: + return normalize_agent_name(value) + # ============================================================================= # Observability Models @@ -116,12 +120,12 @@ class ControlExecutionEventDB(Base): Raw control execution events with minimal indexed columns + JSONB. Schema designed for simplicity and flexibility: - - Only 4 columns: control_execution_id, timestamp, agent_uuid, data + - Only 4 columns: control_execution_id, timestamp, agent_name, data - Full event stored in JSONB 'data' column - Query-time aggregation from JSONB fields - No migrations needed for new event fields - Primary access pattern: (agent_uuid, timestamp DESC) for stats queries. + Primary access pattern: (agent_name, timestamp DESC) for stats queries. Expression index on (data->>'control_id') for grouping. """ @@ -138,9 +142,7 @@ class ControlExecutionEventDB(Base): server_default=text("CURRENT_TIMESTAMP"), nullable=False, ) - agent_uuid: Mapped[_uuid.UUID] = mapped_column( - PG_UUID(as_uuid=True), nullable=False, - ) + agent_name: Mapped[str] = mapped_column(String(255), nullable=False) # Full event data as JSONB data: Mapped[dict[str, Any]] = mapped_column( @@ -149,5 +151,6 @@ class ControlExecutionEventDB(Base): # Composite index for agent + time queries (primary access pattern) __table_args__ = ( - Index("ix_events_agent_time", "agent_uuid", timestamp.desc()), + Index("ix_events_agent_time", "agent_name", timestamp.desc()), + Index("ix_events_data_control_id", text("(data ->> 'control_id'::text)")), ) diff --git a/server/src/agent_control_server/observability/ingest/direct.py b/server/src/agent_control_server/observability/ingest/direct.py index 081cd4be..b2b09e6d 100644 --- a/server/src/agent_control_server/observability/ingest/direct.py +++ b/server/src/agent_control_server/observability/ingest/direct.py @@ -95,7 +95,6 @@ def _log_events(self, events: list[ControlExecutionEvent]) -> None: "event_type": "control_execution", "trace_id": event.trace_id, "span_id": event.span_id, - "agent_uuid": str(event.agent_uuid), "agent_name": event.agent_name, "control_id": event.control_id, "control_name": event.control_name, diff --git a/server/src/agent_control_server/observability/store/base.py b/server/src/agent_control_server/observability/store/base.py index c8bbd712..c73de99c 100644 --- a/server/src/agent_control_server/observability/store/base.py +++ b/server/src/agent_control_server/observability/store/base.py @@ -15,7 +15,6 @@ from abc import ABC, abstractmethod from datetime import timedelta from typing import Literal -from uuid import UUID from agent_control_models.observability import ( ControlExecutionEvent, @@ -134,7 +133,7 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: @abstractmethod async def query_stats( self, - agent_uuid: UUID, + agent_name: str, time_range: timedelta, control_id: int | None = None, include_timeseries: bool = False, @@ -143,7 +142,7 @@ async def query_stats( """Query stats (aggregated at query time from raw events). Args: - agent_uuid: UUID of the agent to query stats for + agent_name: Identifier of the agent to query stats for time_range: Time range to aggregate over (from now) control_id: Optional control ID to filter by include_timeseries: Whether to include time-series data diff --git a/server/src/agent_control_server/observability/store/postgres.py b/server/src/agent_control_server/observability/store/postgres.py index 8838082c..997d8c1b 100644 --- a/server/src/agent_control_server/observability/store/postgres.py +++ b/server/src/agent_control_server/observability/store/postgres.py @@ -12,7 +12,6 @@ import json import logging from datetime import UTC, datetime, timedelta -from uuid import UUID from agent_control_models.observability import ( ControlExecutionEvent, @@ -84,7 +83,7 @@ class PostgresEventStore(EventStore): """PostgreSQL-based event store with JSONB storage and query-time aggregation. This implementation stores raw events with: - - Indexed columns (control_execution_id, timestamp, agent_uuid) for efficient filtering + - Indexed columns (control_execution_id, timestamp, agent_name) for efficient filtering - JSONB 'data' column containing the full event for flexible querying Stats are computed at query time from raw events, which is fast enough @@ -109,7 +108,7 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: The simplified schema stores only 4 columns: - control_execution_id (PK) - timestamp (indexed) - - agent_uuid (indexed) + - agent_name (indexed) - data (JSONB containing full event) Args: @@ -130,7 +129,7 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: values.append({ "control_execution_id": event.control_execution_id, "timestamp": event.timestamp, - "agent_uuid": event.agent_uuid, + "agent_name": event.agent_name, "data": json.dumps(event_data), }) @@ -139,9 +138,9 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: await session.execute( text(""" INSERT INTO control_execution_events ( - control_execution_id, timestamp, agent_uuid, data + control_execution_id, timestamp, agent_name, data ) VALUES ( - :control_execution_id, :timestamp, :agent_uuid, + :control_execution_id, :timestamp, :agent_name, CAST(:data AS JSONB) ) ON CONFLICT (control_execution_id) DO NOTHING @@ -155,7 +154,7 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: async def query_stats( self, - agent_uuid: UUID, + agent_name: str, time_range: timedelta, control_id: int | None = None, include_timeseries: bool = False, @@ -170,7 +169,7 @@ async def query_stats( and LEFT JOINs with aggregated data to include empty buckets. Args: - agent_uuid: UUID of the agent to query stats for + agent_name: identifier of the agent to query stats for time_range: Time range to aggregate over (from now) control_id: Optional control ID to filter by include_timeseries: Whether to include time-series data @@ -183,7 +182,7 @@ async def query_stats( cutoff = now - time_range params: dict = { - "agent_uuid": agent_uuid, + "agent_name": agent_name, "cutoff": cutoff, } @@ -204,7 +203,7 @@ async def query_stats( WITH filtered_events AS ( SELECT timestamp, data FROM control_execution_events - WHERE agent_uuid = :agent_uuid + WHERE agent_name = :agent_name AND timestamp >= :cutoff {control_filter} ), @@ -274,7 +273,7 @@ async def query_stats( NULL::timestamptz as bucket, {SQL_STATS_AGGREGATIONS} FROM control_execution_events - WHERE agent_uuid = :agent_uuid + WHERE agent_name = :agent_name AND timestamp >= :cutoff {control_filter} GROUP BY data->>'control_id', data->>'control_name' @@ -384,11 +383,11 @@ def _timedelta_to_interval(self, td: timedelta) -> str: async def query_events(self, query: EventQueryRequest) -> EventQueryResponse: """Query raw events with filters and pagination. - Supports filtering by trace_id, span_id, agent_uuid, control_ids, + Supports filtering by trace_id, span_id, agent_name, control_ids, actions, matched status, time range, and pagination. Filters use JSONB operators for fields stored in the 'data' column, - except for indexed columns (control_execution_id, timestamp, agent_uuid). + except for indexed columns (control_execution_id, timestamp, agent_name). Args: query: Query parameters (filters, pagination) @@ -405,9 +404,9 @@ async def query_events(self, query: EventQueryRequest) -> EventQueryResponse: where_clauses.append("control_execution_id = :control_execution_id") params["control_execution_id"] = query.control_execution_id - if query.agent_uuid: - where_clauses.append("agent_uuid = :agent_uuid") - params["agent_uuid"] = query.agent_uuid + if query.agent_name: + where_clauses.append("agent_name = :agent_name") + params["agent_name"] = query.agent_name if query.start_time: where_clauses.append("timestamp >= :start_time") diff --git a/server/src/agent_control_server/services/agent_names.py b/server/src/agent_control_server/services/agent_names.py new file mode 100644 index 00000000..606c3707 --- /dev/null +++ b/server/src/agent_control_server/services/agent_names.py @@ -0,0 +1,35 @@ +"""Agent name normalization helpers for server endpoints.""" + +from agent_control_models.agent import normalize_agent_name +from agent_control_models.errors import ErrorCode, ValidationErrorItem + +from ..errors import APIValidationError + + +def normalize_agent_name_or_422( + agent_name: str, + *, + field_name: str = "agent_name", +) -> str: + """Normalize an agent name or raise a standardized 422 validation error.""" + try: + return normalize_agent_name(agent_name) + except ValueError as exc: + raise APIValidationError( + error_code=ErrorCode.VALIDATION_ERROR, + detail="Invalid agent_name", + resource="Agent", + hint=( + "Agent names must be at least 10 characters and may only contain " + "letters, digits, ':', '_' or '-'." + ), + errors=[ + ValidationErrorItem( + resource="Agent", + field=field_name, + code="invalid_format", + message=str(exc), + value=agent_name, + ) + ], + ) from exc diff --git a/server/src/agent_control_server/services/controls.py b/server/src/agent_control_server/services/controls.py index f7867081..46f9987d 100644 --- a/server/src/agent_control_server/services/controls.py +++ b/server/src/agent_control_server/services/controls.py @@ -2,7 +2,6 @@ import logging from collections.abc import Sequence -from uuid import UUID from agent_control_models import ControlDefinition from agent_control_models.errors import ErrorCode, ValidationErrorItem @@ -29,7 +28,7 @@ async def list_controls_for_policy(policy_id: int, db: AsyncSession) -> list[Con async def list_controls_for_agent( - agent_id: UUID, + agent_name: str, db: AsyncSession, *, allow_invalid_step_name_regex: bool = False, @@ -46,7 +45,7 @@ async def list_controls_for_agent( .join(policy_controls, Control.id == policy_controls.c.control_id) .join(Policy, policy_controls.c.policy_id == Policy.id) .join(Agent, Policy.id == Agent.policy_id) - .where(Agent.agent_uuid == agent_id) + .where(Agent.name == agent_name) ) result = await db.execute(stmt) diff --git a/server/src/agent_control_server/services/schema_compat.py b/server/src/agent_control_server/services/schema_compat.py index 796ea41f..99755bc9 100644 --- a/server/src/agent_control_server/services/schema_compat.py +++ b/server/src/agent_control_server/services/schema_compat.py @@ -130,5 +130,5 @@ def format_compatibility_error(evaluator_name: str, errors: list[str]) -> str: return ( f"Evaluator '{evaluator_name}' schema change is not backward compatible. " f"Changes detected: {error_list}. " - f"To make breaking changes, create a new agent with a different UUID/name." + "To make breaking changes, create a new agent with a different name." ) diff --git a/server/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index 13daec4e..2297e9a5 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -19,11 +19,11 @@ def _init_agent( steps: list[dict] | None = None, evaluators: list[dict] | None = None, ) -> tuple[str, str]: - aid = agent_id or str(uuid.uuid4()) - name = agent_name or f"Agent-{uuid.uuid4().hex[:6]}" + name = (agent_name or agent_id or f"agent-{uuid.uuid4().hex[:12]}").lower() + if len(name) < 10: + name = f"{name}-agent".replace("--", "-") payload = { "agent": { - "agent_id": aid, "agent_name": name, "agent_description": "desc", "agent_version": "1.0", @@ -33,7 +33,7 @@ def _init_agent( } resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - return aid, name + return name, name def _create_control_with_data(client: TestClient, data: dict) -> int: @@ -282,7 +282,7 @@ def test_init_agent_rejects_builtin_evaluator_name(client: TestClient) -> None: payload = { "agent": { "agent_id": str(uuid.uuid4()), - "agent_name": f"Agent-{uuid.uuid4().hex[:6]}", + "agent_name": f"agent-{uuid.uuid4().hex[:12]}", "agent_description": "desc", "agent_version": "1.0", }, @@ -300,16 +300,14 @@ def test_init_agent_rejects_builtin_evaluator_name(client: TestClient) -> None: assert resp.json()["error_code"] == "EVALUATOR_NAME_CONFLICT" -def test_init_agent_uuid_conflict_on_same_name(client: TestClient) -> None: +def test_init_agent_same_name_is_idempotent(client: TestClient) -> None: # Given: an existing agent with a specific name - name = f"Agent-{uuid.uuid4().hex[:6]}" - agent_id = str(uuid.uuid4()) - _init_agent(client, agent_id=agent_id, agent_name=name) + name = f"agent-{uuid.uuid4().hex[:12]}" + _init_agent(client, agent_name=name) - # When: re-registering with the same name but a different UUID + # When: re-registering with the same name payload = { "agent": { - "agent_id": str(uuid.uuid4()), "agent_name": name, "agent_description": "desc", "agent_version": "1.0", @@ -318,21 +316,19 @@ def test_init_agent_uuid_conflict_on_same_name(client: TestClient) -> None: } resp = client.post("/api/v1/agents/initAgent", json=payload) - # Then: UUID conflict is returned - assert resp.status_code == 409 - assert resp.json()["error_code"] == "AGENT_UUID_CONFLICT" + # Then: request is idempotent + assert resp.status_code == 200 + assert resp.json()["created"] is False -def test_init_agent_name_conflict_on_same_uuid(client: TestClient) -> None: - # Given: an existing agent with a specific UUID - agent_id = str(uuid.uuid4()) - original_name = f"Agent-{uuid.uuid4().hex[:6]}" - _init_agent(client, agent_id=agent_id, agent_name=original_name) +def test_init_agent_different_name_creates_new_agent(client: TestClient) -> None: + # Given: an existing agent + original_name = f"agent-{uuid.uuid4().hex[:12]}" + _init_agent(client, agent_name=original_name) - # When: re-registering with the same UUID but a different name + # When: registering another agent with a different name payload = { "agent": { - "agent_id": agent_id, "agent_name": f"{original_name}-renamed", "agent_description": "desc", "agent_version": "1.0", @@ -341,9 +337,9 @@ def test_init_agent_name_conflict_on_same_uuid(client: TestClient) -> None: } resp = client.post("/api/v1/agents/initAgent", json=payload) - # Then: name conflict is returned - assert resp.status_code == 409 - assert resp.json()["error_code"] == "AGENT_NAME_CONFLICT" + # Then: a new agent is created + assert resp.status_code == 200 + assert resp.json()["created"] is True def test_list_agent_controls_corrupted_control_data_returns_422( @@ -377,8 +373,8 @@ def test_list_agent_controls_corrupted_control_data_returns_422( def test_list_agents_invalid_cursor_returns_first_page(client: TestClient) -> None: # Given: two agents - _init_agent(client, agent_name=f"Agent-{uuid.uuid4().hex[:6]}") - _init_agent(client, agent_name=f"Agent-{uuid.uuid4().hex[:6]}") + _init_agent(client, agent_name=f"agent-{uuid.uuid4().hex[:12]}") + _init_agent(client, agent_name=f"agent-{uuid.uuid4().hex[:12]}") # When: listing agents without cursor resp = client.get("/api/v1/agents") @@ -400,7 +396,7 @@ def test_list_agent_evaluators_corrupted_data_returns_empty(client: TestClient) agent_id, _ = _init_agent(client, evaluators=[{"name": "eval-a", "config_schema": {}}]) with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": "{\"bad\": \"data\"}", "id": agent_id}, ) @@ -424,7 +420,7 @@ def test_set_agent_policy_rejects_corrupted_agent_data(client: TestClient) -> No with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) @@ -554,7 +550,7 @@ def test_list_agents_corrupted_data_sets_zero_counts(client: TestClient) -> None ) with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) @@ -563,7 +559,7 @@ def test_list_agents_corrupted_data_sets_zero_counts(client: TestClient) -> None # Then: step/evaluator counts are zeroed for corrupted data assert resp.status_code == 200 - agents = {a["agent_id"]: a for a in resp.json()["agents"]} + agents = {a["agent_name"]: a for a in resp.json()["agents"]} agent = agents[agent_id] assert agent["step_count"] == 0 assert agent["evaluator_count"] == 0 @@ -574,7 +570,7 @@ def test_get_agent_corrupted_data_returns_422(client: TestClient) -> None: agent_id, _ = _init_agent(client) with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) @@ -592,7 +588,7 @@ def test_get_agent_corrupted_metadata_returns_422(client: TestClient) -> None: corrupted = {"agent_metadata": {}, "steps": [], "evaluators": []} with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps(corrupted), "id": agent_id}, ) @@ -623,7 +619,7 @@ def test_get_agent_policy_missing_policy_returns_404(client: TestClient) -> None with Session(engine) as session: agent_row = ( session.execute( - select(AgentModel).where(AgentModel.agent_uuid == agent_id) + select(AgentModel).where(AgentModel.name == agent_id) ) .scalars() .first() @@ -718,8 +714,8 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: def test_list_agents_valid_cursor_not_found_returns_first_page(client: TestClient) -> None: # Given: two agents - _init_agent(client, agent_name=f"Agent-{uuid.uuid4().hex[:6]}") - _init_agent(client, agent_name=f"Agent-{uuid.uuid4().hex[:6]}") + _init_agent(client, agent_name=f"agent-{uuid.uuid4().hex[:12]}") + _init_agent(client, agent_name=f"agent-{uuid.uuid4().hex[:12]}") # When: listing without cursor resp = client.get("/api/v1/agents", params={"limit": 1}) @@ -738,8 +734,8 @@ def test_list_agents_valid_cursor_not_found_returns_first_page(client: TestClien def test_init_agent_adds_new_evaluator(client: TestClient) -> None: # Given: an existing agent with one evaluator - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = { "agent": { "agent_id": agent_id, @@ -777,8 +773,8 @@ def test_init_agent_adds_new_evaluator(client: TestClient) -> None: def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> None: # Given: an agent assigned to a policy with a control - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name init_resp = client.post( "/api/v1/agents/initAgent", json={ @@ -828,7 +824,7 @@ def test_patch_agent_corrupted_data_returns_422(client: TestClient) -> None: agent_id, _ = _init_agent(client) with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) @@ -848,7 +844,7 @@ def test_get_agent_evaluator_corrupted_data_returns_404(client: TestClient) -> N agent_id, _ = _init_agent(client, evaluators=[{"name": "eval-a", "config_schema": {}}]) with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) @@ -867,7 +863,7 @@ def test_init_agent_rejects_duplicate_step_names_in_single_request( payload = { "agent": { "agent_id": str(uuid.uuid4()), - "agent_name": f"Agent-{uuid.uuid4().hex[:6]}", + "agent_name": f"agent-{uuid.uuid4().hex[:12]}", "agent_description": "desc", "agent_version": "1.0", }, @@ -895,8 +891,8 @@ def test_init_agent_rejects_step_schema_conflict_across_registrations( client: TestClient, ) -> None: # Given: an agent registered with a step - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name original_payload = { "agent": { "agent_id": agent_id, @@ -949,8 +945,8 @@ def test_init_agent_accepts_identical_step_schema_across_registrations( client: TestClient, ) -> None: # Given: an agent registered with a step - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = { "agent": { "agent_id": agent_id, diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 8d63bf9f..4be88a31 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -358,6 +358,41 @@ def test_list_controls_cursor_with_name_and_enabled_filters(client: TestClient) assert page2["controls"][0]["enabled"] is True +def test_list_controls_includes_used_by_agent_mapping(client: TestClient) -> None: + # Given: one control linked through Policy -> Agent + control_id, control_name = _create_control(client, name=f"Mapped-{uuid.uuid4()}") + _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) + + policy_name = f"pol-{uuid.uuid4()}" + policy_resp = client.put("/api/v1/policies", json={"name": policy_name}) + assert policy_resp.status_code == 200 + policy_id = policy_resp.json()["policy_id"] + + assoc_resp = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") + assert assoc_resp.status_code == 200 + + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + init_resp = client.post( + "/api/v1/agents/initAgent", + json={"agent": {"agent_name": agent_name}, "steps": []}, + ) + assert init_resp.status_code == 200 + + assign_resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") + assert assign_resp.status_code == 200 + + # When: listing controls + resp = client.get("/api/v1/controls", params={"name": "mapped"}) + assert resp.status_code == 200 + controls = resp.json()["controls"] + + # Then: used_by_agent is populated from the join traversal + assert len(controls) == 1 + assert controls[0]["id"] == control_id + assert controls[0]["name"] == control_name + assert controls[0]["used_by_agent"] == {"agent_name": agent_name} + + def test_delete_control_force_dissociates(client: TestClient) -> None: # Given: a control associated with a policy control_id, _ = _create_control(client) @@ -463,8 +498,8 @@ def test_set_control_data_agent_scoped_agent_not_found(client: TestClient) -> No def test_set_control_data_agent_scoped_evaluator_missing(client: TestClient) -> None: # Given: an agent without the referenced evaluator - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ @@ -491,8 +526,8 @@ def test_set_control_data_agent_scoped_evaluator_missing(client: TestClient) -> def test_set_control_data_agent_scoped_invalid_schema(client: TestClient) -> None: # Given: an agent with evaluator schema requiring "pattern" - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ @@ -581,8 +616,8 @@ def test_set_control_data_agent_scoped_corrupted_agent_data_returns_422( client: TestClient, ) -> None: # Given: an agent whose stored data is corrupted - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:6]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ @@ -595,7 +630,7 @@ def test_set_control_data_agent_scoped_corrupted_agent_data_returns_422( with engine.begin() as conn: conn.execute( - text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE agent_uuid = :id"), + text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), {"data": json.dumps({"bad": "data"}), "id": agent_id}, ) diff --git a/server/tests/test_error_handling.py b/server/tests/test_error_handling.py index 5a14d8c3..756827c8 100644 --- a/server/tests/test_error_handling.py +++ b/server/tests/test_error_handling.py @@ -31,11 +31,12 @@ def test_init_agent_rollback_on_create_failure( ) -> None: """Test that init_agent rolls back transaction when commit fails on create.""" # Given: a valid agent init payload - agent_id = str(uuid.uuid4()) + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = { "agent": { "agent_id": agent_id, - "agent_name": f"test-agent-{uuid.uuid4()}", + "agent_name": agent_name, "agent_description": "test", "agent_version": "1.0", "agent_metadata": {}, @@ -72,7 +73,7 @@ def test_delete_agent_policy_rollback_on_failure( } r1 = client.post("/api/v1/agents/initAgent", json=agent_payload) assert r1.status_code == 200 - agent_id = agent_payload["agent"]["agent_id"] + agent_id = agent_payload["agent"]["agent_name"] policy_name = f"test-policy-{uuid.uuid4()}" r2 = client.put("/api/v1/policies", json={"name": policy_name}) @@ -88,7 +89,7 @@ def test_delete_agent_policy_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( - session.query(Agent).filter(Agent.agent_uuid == agent_id).first() + session.query(Agent).filter(Agent.name == agent_id).first() ) assert existing_agent is not None @@ -119,8 +120,8 @@ def test_init_agent_rollback_on_update_failure( ) -> None: """Test that init_agent rolls back transaction when commit fails on update.""" # Given: an existing agent - agent_id = str(uuid.uuid4()) - agent_name = f"test-agent-{uuid.uuid4()}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = { "agent": { "agent_id": agent_id, @@ -219,8 +220,8 @@ def test_patch_agent_rollback_on_failure( ) -> None: """Test that patch_agent rolls back when commit fails.""" # Given: an existing agent with a step to remove - agent_id = str(uuid.uuid4()) - agent_name = f"test-agent-{uuid.uuid4()}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = { "agent": { "agent_id": agent_id, @@ -247,7 +248,7 @@ def test_patch_agent_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( - session.query(Agent).filter(Agent.agent_uuid == agent_id).first() + session.query(Agent).filter(Agent.name == agent_id).first() ) assert existing_agent is not None @@ -358,7 +359,7 @@ def test_set_agent_policy_rollback_on_failure( } r1 = client.post("/api/v1/agents/initAgent", json=agent_payload) assert r1.status_code == 200 - agent_id = agent_payload["agent"]["agent_id"] + agent_id = agent_payload["agent"]["agent_name"] policy_name = f"test-policy-{uuid.uuid4()}" r2 = client.put("/api/v1/policies", json={"name": policy_name}) @@ -372,7 +373,7 @@ def test_set_agent_policy_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( session.query(Agent) - .filter(Agent.agent_uuid == agent_id) + .filter(Agent.name == agent_id) .first() ) existing_policy = ( diff --git a/server/tests/test_evaluation_e2e.py b/server/tests/test_evaluation_e2e.py index b6e414a8..8d3f6ad1 100644 --- a/server/tests/test_evaluation_e2e.py +++ b/server/tests/test_evaluation_e2e.py @@ -19,12 +19,12 @@ def test_evaluation_flow_deny(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data) + agent_name, control_name = create_and_assign_policy(client, control_data) # When: Sending a request containing "secret" payload = Step(type="llm", name="test-step", input="This contains a secret", output=None) req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=payload, stage="pre" ) @@ -41,15 +41,15 @@ def test_evaluation_flow_deny(client: TestClient): def test_evaluation_no_policy(client: TestClient): """Test that an agent with no policy assigned is safe.""" # Given: an agent with no policy assigned - agent_uuid = uuid.uuid4() + agent_name = f"agent-{uuid.uuid4().hex[:12]}" client.post("/api/v1/agents/initAgent", json={ - "agent": {"agent_id": str(agent_uuid), "agent_name": "NoPolicyAgent"}, + "agent": {"agent_name": agent_name}, "steps": [] }) # When: evaluating content for that agent req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="anything", output=None), stage="pre" ) @@ -69,17 +69,17 @@ def test_evaluation_empty_policy(client: TestClient): policy_id = resp.json()["policy_id"] # And: an agent assigned to that policy - agent_uuid = uuid.uuid4() + agent_name = f"agent-{uuid.uuid4().hex[:12]}" client.post("/api/v1/agents/initAgent", json={ - "agent": {"agent_id": str(agent_uuid), "agent_name": "EmptyPolicyAgent"}, + "agent": {"agent_name": agent_name}, "steps": [] }) - client.post(f"/api/v1/agents/{str(agent_uuid)}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # When: evaluating content for that agent req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="anything", output=None), stage="pre" ) @@ -106,12 +106,12 @@ def test_evaluation_path_failure(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="PathFailAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="PathFailAgent") # When: Sending a request payload = Step(type="llm", name="test-step", input="some content", output=None) req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=payload, stage="pre" ) @@ -135,11 +135,11 @@ def test_evaluation_selector_star_uses_full_step_json(client: TestClient): "evaluator": {"name": "json", "config": {"required_fields": ["type"]}}, "action": {"decision": "deny"}, } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="JsonStarAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="JsonStarAgent") # When: evaluating a valid step payload payload = Step(type="llm", name="test-step", input="hello", output=None) - req = EvaluationRequest(agent_uuid=agent_uuid, step=payload, stage="pre") + req = EvaluationRequest(agent_name=agent_name, step=payload, stage="pre") resp = client.post("/api/v1/evaluation", json=req.model_dump(mode="json")) # Then: evaluation is safe (JSON evaluator accepts the full payload) @@ -163,7 +163,7 @@ def test_evaluation_tool_step_nested(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="ToolNestedAgent") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="ToolNestedAgent") # Case 1: Safe value # When: Sending safe nested value @@ -173,7 +173,7 @@ def test_evaluation_tool_step_nested(client: TestClient): output=None ) req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=safe_payload, stage="pre" ) @@ -191,7 +191,7 @@ def test_evaluation_tool_step_nested(client: TestClient): output=None ) req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=unsafe_payload, stage="pre" ) @@ -217,11 +217,11 @@ def test_evaluation_deny_precedence(client: TestClient): "action": {"decision": "warn"} } # Use helper to setup agent with first control - agent_uuid, warn_control_name = create_and_assign_policy(client, control_warn, agent_name="PrecedenceAgent") + agent_name, warn_control_name = create_and_assign_policy(client, control_warn, agent_name="PrecedenceAgent") # Create and add second (Deny) control to the same policy # Actually, easiest is to fetch the agent's policy ID - resp = client.get(f"/api/v1/agents/{agent_uuid}/policy") + resp = client.get(f"/api/v1/agents/{agent_name}/policy") policy_id = resp.json()["policy_id"] # Create Deny Control @@ -243,7 +243,7 @@ def test_evaluation_deny_precedence(client: TestClient): # When: Sending request matching "keyword" req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="This has a keyword", output=None), stage="pre" ) @@ -271,11 +271,11 @@ def test_evaluation_stage_filtering(client: TestClient): "evaluator": {"name": "regex", "config": {"pattern": "bad_output"}}, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="StageAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="StageAgent") # When: evaluating at the pre stage req_pre = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, # Even if we provide output, the control shouldn't run in 'pre' stage? # Actually the control says stage='post'. If we send request with stage='pre', it skips. step=Step(type="llm", name="test-step", input="bad_output", output="bad_output"), @@ -288,7 +288,7 @@ def test_evaluation_stage_filtering(client: TestClient): # When: evaluating at the post stage req_post = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="ok", output="bad_output"), stage="post" ) @@ -310,12 +310,12 @@ def test_evaluation_step_type_filtering(client: TestClient): "evaluator": {"name": "regex", "config": {"pattern": "rm_rf"}}, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="AppliesToAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="AppliesToAgent") # When: evaluating an LLM step (control should not apply) # Note: LLM steps don't have tool names, but the engine filters by step type. req_llm = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="rm_rf", output=None), stage="pre" ) @@ -325,7 +325,7 @@ def test_evaluation_step_type_filtering(client: TestClient): # When: evaluating a tool step (control applies) req_tool = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="rm_rf", input={}), stage="pre" ) @@ -349,11 +349,11 @@ def test_evaluation_denylist_step_name(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="ToolBlockAgent") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="ToolBlockAgent") # When: evaluating a safe tool (not in list) req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="safe_tool", input={}), stage="pre" ) @@ -363,7 +363,7 @@ def test_evaluation_denylist_step_name(client: TestClient): # When: evaluating a dangerous tool (in list) req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="dangerous_tool", input={}), stage="pre" ) diff --git a/server/tests/test_evaluation_e2e_list_evaluator.py b/server/tests/test_evaluation_e2e_list_evaluator.py index 55c23545..4c73c863 100644 --- a/server/tests/test_evaluation_e2e_list_evaluator.py +++ b/server/tests/test_evaluation_e2e_list_evaluator.py @@ -23,12 +23,12 @@ def test_list_evaluator_denylist_behavior(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="DenyListAgent") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="DenyListAgent") # Case 1: Safe Value # When: Sending a tool step with a safe command "ls" req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="shell", input={"cmd": "ls"}, output=None), stage="pre" ) @@ -40,7 +40,7 @@ def test_list_evaluator_denylist_behavior(client: TestClient): # Case 2: Unsafe Value # When: Sending a tool step with a forbidden command "rm" req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="shell", input={"cmd": "rm"}, output=None), stage="pre" ) @@ -71,12 +71,12 @@ def test_list_evaluator_allowlist_behavior(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="AllowListAgent") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="AllowListAgent") # Case 1: Allowed Value # When: Sending a tool step with the allowed tool "safe_tool" req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="safe_tool", input={}, output=None), stage="pre" ) @@ -88,7 +88,7 @@ def test_list_evaluator_allowlist_behavior(client: TestClient): # Case 2: Disallowed Value # When: Sending a tool step with a tool NOT in the list ("unknown_tool") req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="unknown_tool", input={}, output=None), stage="pre" ) @@ -118,11 +118,11 @@ def test_list_evaluator_case_insensitive(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="CaseAgent") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="CaseAgent") # When: Sending input "blockme" (lowercase) req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="blockme", output=None), stage="pre" ) @@ -151,12 +151,12 @@ def test_list_evaluator_list_input_any_match(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="TagAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="TagAgent") # Case 1: List containing restricted item # When: Sending tags ["public", "restricted"] req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="update", input={"tags": ["public", "restricted"]}, output=None), stage="pre" ) @@ -168,7 +168,7 @@ def test_list_evaluator_list_input_any_match(client: TestClient): # Case 2: List containing only safe items # When: Sending tags ["public", "internal"] req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="update", input={"tags": ["public", "internal"]}, output=None), stage="pre" ) @@ -198,12 +198,12 @@ def test_list_evaluator_list_input_all_match(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_data, agent_name="SafeTagAgent") + agent_name, _ = create_and_assign_policy(client, control_data, agent_name="SafeTagAgent") # Case 1: All items match # When: Sending only safe tags req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="update", input={"tags": ["safe_tag", "audit_approved"]}, output=None), stage="pre" ) @@ -215,7 +215,7 @@ def test_list_evaluator_list_input_all_match(client: TestClient): # Case 2: Mixed items (one unsafe) # When: Sending tags with one risky item req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="update", input={"tags": ["safe_tag", "risky"]}, output=None), stage="pre" ) @@ -244,12 +244,12 @@ def test_list_evaluator_disallow_name(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="NoDangerousTools") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="NoDangerousTools") # Case 1: Allowed Tool # When: Calling a safe tool req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="get_user", input={"id": "123"}, output=None), stage="pre" ) @@ -260,7 +260,7 @@ def test_list_evaluator_disallow_name(client: TestClient): # Case 2: Disallowed Tool # When: Calling a dangerous tool req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="delete_user", input={"id": "123"}, output=None), stage="pre" ) @@ -289,12 +289,12 @@ def test_list_evaluator_allow_only_argument_values(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data, agent_name="RegionPolicy") + agent_name, control_name = create_and_assign_policy(client, control_data, agent_name="RegionPolicy") # Case 1: Allowed Value # When: Using an allowed region req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="deploy", input={"region": "us-east-1"}, output=None), stage="pre" ) @@ -305,7 +305,7 @@ def test_list_evaluator_allow_only_argument_values(client: TestClient): # Case 2: Disallowed Value # When: Using a disallowed region req_unsafe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="deploy", input={"region": "eu-central-1"}, output=None), stage="pre" ) @@ -335,11 +335,11 @@ def test_list_evaluator_edge_cases(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_empty, agent_name="EmptyControlAgent") + agent_name, _ = create_and_assign_policy(client, control_empty, agent_name="EmptyControlAgent") # When: Calling any tool req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="something", input={}, output=None), stage="pre" ) @@ -365,11 +365,11 @@ def test_list_evaluator_edge_cases(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_types, agent_name="TypeAgent") + agent_name, control_name = create_and_assign_policy(client, control_types, agent_name="TypeAgent") # When: Input is integer 10 req_int = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="count", input={"count": 10}, output=None), stage="pre" ) @@ -379,7 +379,7 @@ def test_list_evaluator_edge_cases(client: TestClient): # When: Input is string "20" req_str = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="count", input={"count": "20"}, output=None), stage="pre" ) @@ -405,11 +405,11 @@ def test_list_evaluator_edge_cases(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_special, agent_name="SpecialCharAgent") + agent_name, control_name = create_and_assign_policy(client, control_special, agent_name="SpecialCharAgent") # When: Input exactly matches "(test)" req_special = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="search", input={"query": "(test)"}, output=None), stage="pre" ) @@ -419,7 +419,7 @@ def test_list_evaluator_edge_cases(client: TestClient): # When: Input is "test" (without parens) req_normal = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="search", input={"query": "test"}, output=None), stage="pre" ) @@ -446,11 +446,11 @@ def test_list_evaluator_edge_cases(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_null, agent_name="NullAgent") + agent_name, _ = create_and_assign_policy(client, control_null, agent_name="NullAgent") # When: Selector returns None req_null = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="check", input={}, output=None), stage="pre" ) @@ -482,11 +482,11 @@ def test_list_evaluator_re2_corner_cases(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_large, agent_name="LargeListAgent") + agent_name, _ = create_and_assign_policy(client, control_large, agent_name="LargeListAgent") # When: Matching the last item req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="check", input={"item": "target_value"}, output=None), stage="pre" ) @@ -514,11 +514,11 @@ def test_list_evaluator_newline_strictness(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, _ = create_and_assign_policy(client, control_strict, agent_name="StrictAgent") + agent_name, _ = create_and_assign_policy(client, control_strict, agent_name="StrictAgent") # When: Sending "exact\n" (trailing newline) req_newline = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="check", input={"val": "exact\n"}, output=None), stage="pre" ) diff --git a/server/tests/test_evaluation_e2e_sql_evaluator.py b/server/tests/test_evaluation_e2e_sql_evaluator.py index 45e87791..e82bdb7b 100644 --- a/server/tests/test_evaluation_e2e_sql_evaluator.py +++ b/server/tests/test_evaluation_e2e_sql_evaluator.py @@ -30,13 +30,13 @@ def test_sql_read_only_agent(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="ReadOnlyAgent" ) # When: evaluating a SELECT with LIMIT 100 req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users LIMIT 100"}, @@ -51,7 +51,7 @@ def test_sql_read_only_agent(client: TestClient): # When: evaluating an INSERT query (not allowed) req_insert = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "INSERT INTO users (name) VALUES ('test')"}, @@ -67,7 +67,7 @@ def test_sql_read_only_agent(client: TestClient): # When: evaluating a SELECT without LIMIT req_no_limit = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users"}, @@ -82,7 +82,7 @@ def test_sql_read_only_agent(client: TestClient): # When: evaluating a SELECT with LIMIT 5000 (exceeds max_limit) req_high_limit = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users LIMIT 5000"}, @@ -114,13 +114,13 @@ def test_sql_multi_tenant_security(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="MultiTenantAgent" ) # When: evaluating a query with tenant_id in WHERE req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM orders WHERE tenant_id = 123"}, @@ -135,7 +135,7 @@ def test_sql_multi_tenant_security(client: TestClient): # When: evaluating a query without tenant_id in WHERE req_no_tenant = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM orders WHERE status = 'active'"}, @@ -151,7 +151,7 @@ def test_sql_multi_tenant_security(client: TestClient): # When: evaluating a query with tenant_id only in SELECT req_select_only = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT tenant_id, name FROM orders"}, @@ -182,13 +182,13 @@ def test_sql_block_destructive_operations(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="SafeAgent" ) # When: evaluating a safe SELECT query req_select = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users"}, @@ -203,7 +203,7 @@ def test_sql_block_destructive_operations(client: TestClient): # When: evaluating a non-destructive INSERT query req_insert = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "INSERT INTO logs (message) VALUES ('test')"}, @@ -218,7 +218,7 @@ def test_sql_block_destructive_operations(client: TestClient): # When: evaluating a DROP TABLE query req_drop = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "DROP TABLE users"}, @@ -234,7 +234,7 @@ def test_sql_block_destructive_operations(client: TestClient): # When: evaluating a DELETE query req_delete = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "DELETE FROM users WHERE id = 1"}, @@ -249,7 +249,7 @@ def test_sql_block_destructive_operations(client: TestClient): # When: evaluating a TRUNCATE TABLE query req_truncate = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "TRUNCATE TABLE logs"}, @@ -280,13 +280,13 @@ def test_sql_table_restrictions(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="AnalyticsAgent" ) # When: evaluating a query against an allowed table (users) req_users = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users"}, @@ -301,7 +301,7 @@ def test_sql_table_restrictions(client: TestClient): # When: evaluating a query against an allowed table (orders) req_orders = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM orders"}, @@ -316,7 +316,7 @@ def test_sql_table_restrictions(client: TestClient): # When: evaluating a query against a disallowed table (admin_data) req_admin = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM admin_data"}, @@ -332,7 +332,7 @@ def test_sql_table_restrictions(client: TestClient): # When: evaluating a query against a disallowed table (sensitive_data) req_sensitive = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM sensitive_data"}, @@ -363,13 +363,13 @@ def test_sql_multi_statement_blocking(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="SingleStatementAgent" ) # When: evaluating a single-statement query req_single = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users WHERE id = 1"}, @@ -384,7 +384,7 @@ def test_sql_multi_statement_blocking(client: TestClient): # When: evaluating a multi-statement query req_multi = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users; DROP TABLE users;"}, @@ -417,13 +417,13 @@ def test_sql_limit_enforcement(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="LimitAgent" ) # When: evaluating a SELECT with LIMIT 500 req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users LIMIT 500"}, @@ -438,7 +438,7 @@ def test_sql_limit_enforcement(client: TestClient): # When: evaluating a SELECT with LIMIT 1000 (boundary) req_boundary = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users LIMIT 1000"}, @@ -453,7 +453,7 @@ def test_sql_limit_enforcement(client: TestClient): # When: evaluating a SELECT with LIMIT 1001 (exceeds max) req_exceed = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users LIMIT 1001"}, @@ -469,7 +469,7 @@ def test_sql_limit_enforcement(client: TestClient): # When: evaluating a SELECT without LIMIT req_no_limit = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "SELECT * FROM users"}, @@ -484,7 +484,7 @@ def test_sql_limit_enforcement(client: TestClient): # When: evaluating an INSERT without LIMIT (LIMIT only applies to SELECT) req_insert = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="tool", name="execute_sql", input={"query": "INSERT INTO users (name) VALUES ('test')"}, @@ -522,13 +522,13 @@ def test_sql_llm_output_validation_read_only(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="LlmReadOnlyAgent" ) # When: LLM outputs SELECT with LIMIT req_safe = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Generate a query to get all users", output="SELECT * FROM users LIMIT 10" @@ -542,7 +542,7 @@ def test_sql_llm_output_validation_read_only(client: TestClient): # When: LLM outputs DELETE req_delete = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Delete user with id 1", output="DELETE FROM users WHERE id = 1" @@ -557,7 +557,7 @@ def test_sql_llm_output_validation_read_only(client: TestClient): # When: LLM outputs SELECT without LIMIT req_no_limit = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get all users", output="SELECT * FROM users" @@ -587,13 +587,13 @@ def test_sql_llm_output_multi_statement_blocking(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="LlmSingleStatementAgent" ) # When: LLM outputs a single statement req_single = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get user by id", output="SELECT * FROM users WHERE id = 1" @@ -607,7 +607,7 @@ def test_sql_llm_output_multi_statement_blocking(client: TestClient): # When: LLM outputs a multi-statement query req_multi = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get users and drop table", output="SELECT * FROM users; DROP TABLE users;" @@ -638,13 +638,13 @@ def test_sql_llm_output_table_restrictions(client: TestClient): }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy( + agent_name, control_name = create_and_assign_policy( client, control_data, agent_name="LlmAnalyticsAgent" ) # When: LLM outputs a query on an allowed table (analytics) req_analytics = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get analytics data", output="SELECT * FROM analytics WHERE date > '2024-01-01'" @@ -658,7 +658,7 @@ def test_sql_llm_output_table_restrictions(client: TestClient): # When: LLM outputs a query on an allowed table (reports) req_reports = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get monthly reports", output="SELECT * FROM reports WHERE month = 'January'" @@ -672,7 +672,7 @@ def test_sql_llm_output_table_restrictions(client: TestClient): # When: LLM outputs a query on a disallowed table (users) req_users = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=Step(type="llm", name="test-step", input="Get all users", output="SELECT * FROM users" diff --git a/server/tests/test_evaluation_error_handling.py b/server/tests/test_evaluation_error_handling.py index 1b8e652e..c984dfdc 100644 --- a/server/tests/test_evaluation_error_handling.py +++ b/server/tests/test_evaluation_error_handling.py @@ -17,18 +17,16 @@ def test_evaluation_with_agent_scoped_evaluator_missing(client: TestClient): Then: Returns 400 with clear error message """ # Given: an agent without evaluators - agent_uuid = uuid.uuid4() + agent_name = f"testagent-{uuid.uuid4().hex[:12]}" client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": str(agent_uuid), - "agent_name": f"TestAgent-{uuid.uuid4().hex[:8]}" + "agent_name": agent_name }, "steps": [], "evaluators": [] }) # And: a control referencing a non-existent agent evaluator - agent_name = f"TestAgent-{uuid.uuid4().hex[:8]}" control_data = { "description": "Test control", "enabled": True, @@ -112,7 +110,7 @@ def test_evaluation_errors_field_populated_on_evaluator_failure( }, "action": {"decision": "deny"} } - agent_uuid, control_name = create_and_assign_policy(client, control_data) + agent_name, control_name = create_and_assign_policy(client, control_data) # And: an evaluator instance that throws during evaluation mock_evaluator = MagicMock() @@ -130,7 +128,7 @@ def mock_get_evaluator_instance(config): # When: sending an evaluation request payload = Step(type="llm", name="test-step", input="test content", output=None) req = EvaluationRequest( - agent_uuid=agent_uuid, + agent_name=agent_name, step=payload, stage="pre" ) @@ -173,7 +171,7 @@ def test_evaluation_engine_value_error_returns_422(client: TestClient, monkeypat "evaluator": {"name": "regex", "config": {"pattern": "test"}}, "action": {"decision": "deny"}, } - agent_uuid, _ = create_and_assign_policy(client, control_data) + agent_name, _ = create_and_assign_policy(client, control_data) # And: the engine raises a ValueError during processing import agent_control_engine.core as core_module @@ -185,7 +183,7 @@ async def raise_value_error(*_args, **_kwargs): # When: sending an evaluation request payload = Step(type="llm", name="test-step", input="test content", output=None) - req = EvaluationRequest(agent_uuid=agent_uuid, step=payload, stage="pre") + req = EvaluationRequest(agent_name=agent_name, step=payload, stage="pre") resp = client.post("/api/v1/evaluation", json=req.model_dump(mode="json")) # Then: a validation error is returned @@ -200,7 +198,7 @@ def test_evaluation_warns_when_observability_drops_events( client: TestClient, app, caplog ) -> None: # Given: an agent with a control that will match - agent_uuid, _ = create_and_assign_policy(client) + agent_name, _ = create_and_assign_policy(client) class DroppingIngestor: async def ingest(self, events): # type: ignore[no-untyped-def] @@ -214,7 +212,7 @@ async def ingest(self, events): # type: ignore[no-untyped-def] # When: sending an evaluation request payload = Step(type="llm", name="test-step", input="x", output=None) - req = EvaluationRequest(agent_uuid=agent_uuid, step=payload, stage="pre") + req = EvaluationRequest(agent_name=agent_name, step=payload, stage="pre") resp = client.post("/api/v1/evaluation", json=req.model_dump(mode="json")) # Then: the evaluation succeeds but logs a dropped-events warning diff --git a/server/tests/test_evaluator_schemas.py b/server/tests/test_evaluator_schemas.py index a63e563b..785f41bf 100644 --- a/server/tests/test_evaluator_schemas.py +++ b/server/tests/test_evaluator_schemas.py @@ -14,14 +14,17 @@ def make_agent_payload( evaluators: list | None = None, ): """Helper to create agent payload with evaluators.""" - if agent_id is None: - agent_id = str(uuid.uuid4()) - if name is None: - name = f"Test Agent {uuid.uuid4().hex[:8]}" + if agent_id is not None: + name = agent_id + elif name is None: + name = f"agent-{uuid.uuid4().hex[:12]}" + canonical_name = name.lower().replace(" ", "-") + if len(canonical_name) < 10: + canonical_name = f"{canonical_name}-agent".replace("--", "-") return { "agent": { - "agent_id": agent_id, - "agent_name": name, + "agent_id": canonical_name, + "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", }, diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index b63e1acb..2bc46e5e 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -18,11 +18,13 @@ def make_agent_payload( agent_id: str | None = None, - name: str = "Test Agent", + name: str = "testagent0001", steps: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: - if agent_id is None: - agent_id = str(uuid.uuid4()) + resolved_name = name if name != "testagent0001" else (agent_id or f"agent-{uuid.uuid4().hex[:12]}") + canonical_name = resolved_name.lower().replace(" ", "-") + if len(canonical_name) < 10: + canonical_name = f"{canonical_name}-agent".replace("--", "-") if steps is None: steps = [ { @@ -34,8 +36,8 @@ def make_agent_payload( ] return { "agent": { - "agent_id": agent_id, - "agent_name": name, + "agent_id": canonical_name, + "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", "agent_metadata": {"env": "test"}, @@ -51,7 +53,7 @@ def test_init_agent_route_exists(app: FastAPI) -> None: # (computation done above to gather all paths) # Then: initAgent and agent retrieval endpoints are present assert "/api/v1/agents/initAgent" in paths - assert "/api/v1/agents/{agent_id}" in paths + assert "/api/v1/agents/{agent_name}" in paths def test_init_agent_creates_and_gets_agent(client: TestClient) -> None: @@ -65,17 +67,59 @@ def test_init_agent_creates_and_gets_agent(client: TestClient) -> None: assert body["created"] is True assert body["controls"] == [] - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: retrieving the agent by id - resp2 = client.get(f"/api/v1/agents/{agent_id}") + resp2 = client.get(f"/api/v1/agents/{agent_name}") assert resp2.status_code == 200 data = resp2.json() # Then: stored agent fields match the request - assert data["agent"]["agent_id"] == agent_id assert data["agent"]["agent_name"] == payload["agent"]["agent_name"] assert {s["name"] for s in data["steps"]} == {payload["steps"][0]["name"]} +def test_agent_endpoints_normalize_mixed_case_agent_name(client: TestClient) -> None: + # Given: an agent registered with mixed-case identifier + mixed_case_name = "Agent-PathNorm01" + payload = { + "agent": { + "agent_name": mixed_case_name, + "agent_description": "desc", + "agent_version": "1.0", + }, + "steps": [], + } + init_resp = client.post("/api/v1/agents/initAgent", json=payload) + assert init_resp.status_code == 200 + + # When: hitting path-based agent endpoints using mixed-case path params + get_resp = client.get(f"/api/v1/agents/{mixed_case_name}") + assert get_resp.status_code == 200 + assert get_resp.json()["agent"]["agent_name"] == mixed_case_name.lower() + + policy_id = _create_policy(client) + set_policy_resp = client.post(f"/api/v1/agents/{mixed_case_name}/policy/{policy_id}") + assert set_policy_resp.status_code == 200 + + get_policy_resp = client.get(f"/api/v1/agents/{mixed_case_name}/policy") + assert get_policy_resp.status_code == 200 + assert get_policy_resp.json()["policy_id"] == policy_id + + controls_resp = client.get(f"/api/v1/agents/{mixed_case_name}/controls") + assert controls_resp.status_code == 200 + + evaluators_resp = client.get(f"/api/v1/agents/{mixed_case_name}/evaluators") + assert evaluators_resp.status_code == 200 + + patch_resp = client.patch( + f"/api/v1/agents/{mixed_case_name}", + json={"remove_steps": [], "remove_evaluators": []}, + ) + assert patch_resp.status_code == 200 + + delete_policy_resp = client.delete(f"/api/v1/agents/{mixed_case_name}/policy") + assert delete_policy_resp.status_code == 200 + + def test_init_agent_idempotent_same_steps(client: TestClient) -> None: # Given: an init payload payload = make_agent_payload() @@ -100,11 +144,10 @@ def test_init_agent_updates_metadata_on_reinit(client: TestClient) -> None: Then: The new metadata is persisted """ # Given: create initial agent - agent_id = str(uuid.uuid4()) + agent_name = "metadatatestagent" initial_payload = { "agent": { - "agent_id": agent_id, - "agent_name": "MetadataTestAgent", + "agent_name": agent_name, "agent_description": "Original description", "agent_version": "1.0.0", "agent_metadata": {"env": "dev"}, @@ -118,8 +161,7 @@ def test_init_agent_updates_metadata_on_reinit(client: TestClient) -> None: # When: re-init with updated metadata updated_payload = { "agent": { - "agent_id": agent_id, - "agent_name": "MetadataTestAgent", + "agent_name": agent_name, "agent_description": "Updated description", "agent_version": "2.0.0", "agent_metadata": {"env": "prod", "new_field": "value"}, @@ -131,7 +173,7 @@ def test_init_agent_updates_metadata_on_reinit(client: TestClient) -> None: assert r2.json()["created"] is False # Then: verify metadata is updated - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") assert get_resp.status_code == 200 agent_data = get_resp.json()["agent"] assert agent_data["agent_description"] == "Updated description" @@ -429,12 +471,11 @@ def test_list_agent_controls_agent_not_found_404(client: TestClient) -> None: assert r.status_code == 404 -def test_init_agent_rejects_non_uuid_agent_id(client: TestClient) -> None: - # Given: a payload with an invalid (non-UUID) agent_id +def test_init_agent_rejects_invalid_agent_name(client: TestClient) -> None: + # Given: a payload with an invalid agent_name payload = { "agent": { - "agent_id": "not-a-valid-uuid", - "agent_name": "Test Agent", + "agent_name": "short", "agent_description": "desc", "agent_version": "1.0", }, @@ -469,16 +510,14 @@ def test_list_agents_empty(client: TestClient) -> None: def test_list_agents_returns_created_agents(client: TestClient) -> None: """Test listing agents returns created agents with correct summaries.""" # Given: two agents with different steps/evaluators - agent1_id = str(uuid.uuid4()) - payload1 = make_agent_payload(agent_id=agent1_id, name="Agent One") + payload1 = make_agent_payload(name="agent-one-01") payload1["evaluators"] = [ {"name": "eval-1", "description": "Test", "config_schema": {}}, ] r1 = client.post("/api/v1/agents/initAgent", json=payload1) assert r1.status_code == 200 - agent2_id = str(uuid.uuid4()) - payload2 = make_agent_payload(agent_id=agent2_id, name="Agent Two") + payload2 = make_agent_payload(name="agent-two-02") payload2["steps"] = [ {"type": "tool", "name": "tool_x", "input_schema": {}, "output_schema": {}}, {"type": "tool", "name": "tool_y", "input_schema": {}, "output_schema": {}}, @@ -495,18 +534,18 @@ def test_list_agents_returns_created_agents(client: TestClient) -> None: assert len(body["agents"]) == 2 # Verify agent summaries contain correct data - agent_map = {a["agent_id"]: a for a in body["agents"]} + agent_map = {a["agent_name"]: a for a in body["agents"]} - assert agent1_id in agent_map - agent1 = agent_map[agent1_id] - assert agent1["agent_name"] == "Agent One" + assert "agent-one-01" in agent_map + agent1 = agent_map["agent-one-01"] + assert agent1["agent_name"] == "agent-one-01" assert agent1["step_count"] == 1 # from make_agent_payload assert agent1["evaluator_count"] == 1 assert agent1["policy_id"] is None - assert agent2_id in agent_map - agent2 = agent_map[agent2_id] - assert agent2["agent_name"] == "Agent Two" + assert "agent-two-02" in agent_map + agent2 = agent_map["agent-two-02"] + assert agent2["agent_name"] == "agent-two-02" assert agent2["step_count"] == 2 assert agent2["evaluator_count"] == 0 assert agent2["policy_id"] is None @@ -575,6 +614,31 @@ def test_list_agents_pagination(client: TestClient) -> None: assert body3["pagination"]["next_cursor"] is None +def test_list_agents_accepts_mixed_case_cursor(client: TestClient) -> None: + # Given: three agents to paginate + for i in range(3): + payload = make_agent_payload(name=f"agent-cursor-{i:02d}") + resp = client.post("/api/v1/agents/initAgent", json=payload) + assert resp.status_code == 200 + + first_page = client.get("/api/v1/agents?limit=1") + assert first_page.status_code == 200 + first_body = first_page.json() + assert first_body["pagination"]["has_more"] is True + assert first_body["pagination"]["next_cursor"] is not None + + mixed_case_cursor = str(first_body["pagination"]["next_cursor"]).upper() + + # When: requesting second page with mixed-case cursor + second_page = client.get(f"/api/v1/agents?limit=1&cursor={mixed_case_cursor}") + assert second_page.status_code == 200 + second_body = second_page.json() + + # Then: cursor is normalized and pagination advances + assert len(second_body["agents"]) == 1 + assert second_body["agents"][0]["agent_name"] != first_body["agents"][0]["agent_name"] + + def test_list_agents_limit_clamping(client: TestClient) -> None: """Test that limit is clamped to valid range.""" # Given: one agent diff --git a/server/tests/test_init_agent_conflict_mode.py b/server/tests/test_init_agent_conflict_mode.py index f8efa3eb..0cd1b714 100644 --- a/server/tests/test_init_agent_conflict_mode.py +++ b/server/tests/test_init_agent_conflict_mode.py @@ -21,10 +21,11 @@ def _init_payload( evaluators: list[dict[str, Any]] | None = None, conflict_mode: str | None = None, ) -> dict[str, Any]: + canonical_name = agent_name.lower() payload: dict[str, Any] = { "agent": { - "agent_id": agent_id, - "agent_name": agent_name, + "agent_id": canonical_name, + "agent_name": canonical_name, "agent_description": agent_description, "agent_version": agent_version, }, @@ -71,8 +72,8 @@ def _create_policy_with_agent_evaluator_control( def test_init_agent_overwrite_replaces_steps_and_evaluators(client: TestClient) -> None: # Given: an existing agent registration with baseline steps and evaluators. - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name create_payload = _init_payload( agent_id=agent_id, @@ -163,8 +164,8 @@ def test_init_agent_overwrite_replaces_steps_and_evaluators(client: TestClient) def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: TestClient) -> None: # Given: an agent whose assigned policy contains a control referencing an agent evaluator. - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name evaluator_name = "custom-eval" init_resp = client.post( @@ -215,8 +216,8 @@ def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: Test def test_init_agent_overwrite_noop_reports_not_applied(client: TestClient) -> None: # Given: an existing agent registration and an equivalent overwrite payload. - agent_id = str(uuid.uuid4()) - agent_name = f"Agent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name payload = _init_payload( agent_id=agent_id, agent_name=agent_name, diff --git a/server/tests/test_init_agent_force_replace.py b/server/tests/test_init_agent_force_replace.py index 76358a17..f324a641 100644 --- a/server/tests/test_init_agent_force_replace.py +++ b/server/tests/test_init_agent_force_replace.py @@ -18,8 +18,8 @@ def test_init_agent_force_replace_default_false_works_normally(client: TestClien Then: Creates agent normally (force_replace defaults to False) """ # Given: New agent - agent_id = str(uuid.uuid4()) - agent_name = f"TestAgent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name # When: Create without force_replace (default) resp = client.post("/api/v1/agents/initAgent", json={ @@ -45,8 +45,8 @@ def test_init_agent_force_replace_false_explicit_works_normally(client: TestClie Then: Creates agent normally """ # Given: New agent - agent_id = str(uuid.uuid4()) - agent_name = f"TestAgent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name # When: Create with force_replace=false resp = client.post("/api/v1/agents/initAgent", json={ @@ -73,8 +73,8 @@ def test_init_agent_force_replace_true_on_valid_data_works_normally(client: Test Then: Updates normally without data loss """ # Given: Create agent with steps - agent_id = str(uuid.uuid4()) - agent_name = f"TestAgent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name resp = client.post("/api/v1/agents/initAgent", json={ "agent": { @@ -130,8 +130,8 @@ def test_init_agent_force_replace_true_on_valid_data_works_normally(client: Test def test_init_agent_force_replace_recovers_from_corrupted_data(client: TestClient) -> None: """Test that force_replace=true replaces corrupted stored data.""" # Given: an existing agent with corrupted data in the DB - agent_id = str(uuid.uuid4()) - agent_name = f"TestAgent-{uuid.uuid4().hex[:8]}" + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_id = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ @@ -147,7 +147,7 @@ def test_init_agent_force_replace_recovers_from_corrupted_data(client: TestClien assert resp.status_code == 200 with Session(engine) as session: - agent = session.execute(select(Agent).where(Agent.agent_uuid == agent_id)).scalar_one() + agent = session.execute(select(Agent).where(Agent.name == agent_name)).scalar_one() agent.data = {"steps": "not-a-list"} session.commit() diff --git a/server/tests/test_new_features.py b/server/tests/test_new_features.py index 9d618ede..01bc96ec 100644 --- a/server/tests/test_new_features.py +++ b/server/tests/test_new_features.py @@ -12,14 +12,17 @@ def make_agent_payload( evaluators: list | None = None, ): """Helper to create agent payload.""" - if agent_id is None: - agent_id = str(uuid.uuid4()) - if name is None: - name = f"Test Agent {uuid.uuid4().hex[:8]}" + if agent_id is not None: + name = agent_id + elif name is None: + name = f"agent-{uuid.uuid4().hex[:12]}" + canonical_name = name.lower().replace(" ", "-") + if len(canonical_name) < 10: + canonical_name = f"{canonical_name}-agent".replace("--", "-") return { "agent": { - "agent_id": agent_id, - "agent_name": name, + "agent_id": canonical_name, + "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", }, @@ -307,8 +310,8 @@ def test_policy_assignment_with_builtin_evaluator(client: TestClient) -> None: def test_policy_assignment_with_registered_agent_evaluator(client: TestClient) -> None: """Given an agent with custom evaluator and matching policy, when assigning policy, then succeeds.""" # Given: - agent_id = str(uuid.uuid4()) - agent_name = f"Test Agent {uuid.uuid4().hex[:8]}" + agent_id = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_id payload = make_agent_payload( agent_id=agent_id, name=agent_name, @@ -339,8 +342,8 @@ def test_policy_assignment_with_registered_agent_evaluator(client: TestClient) - def test_control_creation_with_unregistered_evaluator_fails(client: TestClient) -> None: """Given an agent without evaluator, when setting control to use that evaluator, then fails.""" # Given: - agent_id = str(uuid.uuid4()) - agent_name = f"Test Agent {uuid.uuid4().hex[:8]}" + agent_id = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_id payload = make_agent_payload(agent_id=agent_id, name=agent_name) client.post("/api/v1/agents/initAgent", json=payload) @@ -371,8 +374,8 @@ def test_control_creation_with_unregistered_evaluator_fails(client: TestClient) def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> None: """Given policy with Agent A's evaluator, when assigning to Agent B, then fails.""" # Given: Agent A has evaluator, Agent B does not - agent_a_id = str(uuid.uuid4()) - agent_a_name = f"Agent-A-{uuid.uuid4().hex[:8]}" + agent_a_id = f"agent-a-{uuid.uuid4().hex[:12]}" + agent_a_name = agent_a_id payload_a = make_agent_payload( agent_id=agent_a_id, name=agent_a_name, @@ -380,8 +383,8 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No ) client.post("/api/v1/agents/initAgent", json=payload_a) - agent_b_id = str(uuid.uuid4()) - agent_b_name = f"Agent-B-{uuid.uuid4().hex[:8]}" + agent_b_id = f"agent-b-{uuid.uuid4().hex[:12]}" + agent_b_name = agent_b_id payload_b = make_agent_payload(agent_id=agent_b_id, name=agent_b_name) client.post("/api/v1/agents/initAgent", json=payload_b) @@ -537,8 +540,8 @@ def test_patch_agent_remove_evaluator_blocked_by_control(client: TestClient) -> Then: Returns 409 with error message about referencing control """ # Given: Agent with custom evaluator - agent_id = str(uuid.uuid4()) - agent_name = f"Test Agent {uuid.uuid4().hex[:8]}" + agent_id = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_id payload = make_agent_payload( agent_id=agent_id, name=agent_name, @@ -588,8 +591,8 @@ def test_patch_agent_remove_evaluator_allowed_without_policy(client: TestClient) Then: Succeeds since no controls can reference it """ # Given: Agent with custom evaluator but no policy - agent_id = str(uuid.uuid4()) - agent_name = f"Test Agent {uuid.uuid4().hex[:8]}" + agent_id = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_id payload = make_agent_payload( agent_id=agent_id, name=agent_name, diff --git a/server/tests/test_observability_direct_ingest.py b/server/tests/test_observability_direct_ingest.py index 23d3e045..f0d3b964 100644 --- a/server/tests/test_observability_direct_ingest.py +++ b/server/tests/test_observability_direct_ingest.py @@ -15,7 +15,7 @@ class FailingStore(EventStore): async def store(self, events: list[ControlExecutionEvent]) -> int: raise RuntimeError("boom") - async def query_stats(self, agent_uuid, time_range, control_id=None): # pragma: no cover - not used + async def query_stats(self, agent_name, time_range, control_id=None): # pragma: no cover - not used raise NotImplementedError async def query_events(self, query): # pragma: no cover - not used @@ -30,7 +30,7 @@ async def store(self, events: list[ControlExecutionEvent]) -> int: self.calls.append(events) return len(events) - async def query_stats(self, agent_uuid, time_range, control_id=None): # pragma: no cover - not used + async def query_stats(self, agent_name, time_range, control_id=None): # pragma: no cover - not used raise NotImplementedError async def query_events(self, query): # pragma: no cover - not used @@ -45,8 +45,7 @@ async def test_direct_ingestor_drops_on_store_error() -> None: ControlExecutionEvent( trace_id="a" * 32, span_id="b" * 16, - agent_uuid=uuid4(), - agent_name="agent", + agent_name="agent-test-01", control_id=1, control_name="c", check_stage="pre", @@ -74,8 +73,7 @@ async def test_direct_ingestor_logs_when_enabled(caplog: pytest.LogCaptureFixtur event = ControlExecutionEvent( trace_id="a" * 32, span_id="b" * 16, - agent_uuid=uuid4(), - agent_name="agent", + agent_name="agent-test-01", control_id=1, control_name="c", check_stage="pre", diff --git a/server/tests/test_observability_endpoints.py b/server/tests/test_observability_endpoints.py index ec96400f..528b3846 100644 --- a/server/tests/test_observability_endpoints.py +++ b/server/tests/test_observability_endpoints.py @@ -17,7 +17,7 @@ def create_test_event( control_id: int = 1, - agent_uuid: str | UUID | None = None, + agent_name: str | UUID | None = None, action: str = "allow", matched: bool = False, timestamp: datetime | None = None, @@ -27,8 +27,7 @@ def create_test_event( return ControlExecutionEvent( trace_id="a" * 32, # 128-bit hex (32 chars) span_id="b" * 16, # 64-bit hex (16 chars) - agent_uuid=agent_uuid or uuid4(), - agent_name="test-agent", + agent_name=agent_name or f"agent-{uuid4().hex[:12]}", control_id=control_id, control_name=f"control-{control_id}", check_stage="pre", @@ -107,7 +106,7 @@ def test_query_request_with_filters(self): """Test query request with various filters.""" request = EventQueryRequest( trace_id="a" * 32, - agent_uuid=uuid4(), + agent_name=f"agent-{uuid4().hex[:12]}", control_ids=[1, 2, 3], actions=["allow", "deny"], matched=True, @@ -153,8 +152,7 @@ def test_event_with_all_fields(self): event = ControlExecutionEvent( trace_id="a" * 32, span_id="b" * 16, - agent_uuid=uuid4(), - agent_name="test-agent", + agent_name="test-agent", control_id=1, control_name="test-control", check_stage="post", @@ -222,19 +220,40 @@ async def test_ingest_via_direct_ingestor(self, setup_observability): class TestStatsTimeseries: """Tests for time-series stats functionality.""" + @pytest.mark.asyncio + async def test_stats_normalize_mixed_case_agent_name_query( + self, client: TestClient, setup_observability + ): + """Mixed-case agent_name query params are normalized.""" + store = setup_observability + normalized_name = "agent-statsnorm01" + + event = create_test_event(agent_name=normalized_name, matched=True) + await store.store([event]) + + response = client.get( + "/api/v1/observability/stats", + params={"agent_name": "Agent-StatsNorm01", "time_range": "1h"}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["agent_name"] == normalized_name + assert body["totals"]["execution_count"] == 1 + @pytest.mark.asyncio async def test_stats_without_timeseries(self, client: TestClient, setup_observability): """Default response has no timeseries.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" # Create and store an event - event = create_test_event(agent_uuid=agent_uuid, matched=True) + event = create_test_event(agent_name=agent_name, matched=True) await store.store([event]) response = client.get( "/api/v1/observability/stats", - params={"agent_uuid": str(agent_uuid), "time_range": "1h"}, + params={"agent_name": str(agent_name), "time_range": "1h"}, ) assert response.status_code == 200 @@ -246,27 +265,27 @@ async def test_stats_without_timeseries(self, client: TestClient, setup_observab async def test_stats_with_timeseries(self, client: TestClient, setup_observability): """With include_timeseries=true, returns buckets.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create events spread across time events = [ create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="allow", timestamp=now - timedelta(minutes=30), execution_duration_ms=10.0, ), create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="deny", timestamp=now - timedelta(minutes=15), execution_duration_ms=20.0, ), create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=False, timestamp=now - timedelta(minutes=5), ), @@ -277,7 +296,7 @@ async def test_stats_with_timeseries(self, client: TestClient, setup_observabili response = client.get( "/api/v1/observability/stats", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "1h", "include_timeseries": "true", }, @@ -303,12 +322,12 @@ async def test_stats_with_timeseries(self, client: TestClient, setup_observabili async def test_timeseries_bucket_count_1h(self, client: TestClient, setup_observability): """Verify reasonable number of buckets for 1h time range (5m buckets).""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create a single event event = create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, timestamp=now - timedelta(minutes=30), ) @@ -318,7 +337,7 @@ async def test_timeseries_bucket_count_1h(self, client: TestClient, setup_observ response = client.get( "/api/v1/observability/stats", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "1h", "include_timeseries": "true", }, @@ -334,12 +353,12 @@ async def test_timeseries_bucket_count_1h(self, client: TestClient, setup_observ async def test_timeseries_bucket_count_5m(self, client: TestClient, setup_observability): """Verify reasonable number of buckets for 5m time range (30s buckets).""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create a single event event = create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, timestamp=now - timedelta(minutes=2), ) @@ -349,7 +368,7 @@ async def test_timeseries_bucket_count_5m(self, client: TestClient, setup_observ response = client.get( "/api/v1/observability/stats", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "5m", "include_timeseries": "true", }, @@ -367,28 +386,28 @@ async def test_timeseries_aggregates_events_per_bucket( ): """Events in the same bucket are aggregated.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create multiple events in the same 5-minute bucket base_time = now - timedelta(minutes=10) events = [ create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="allow", timestamp=base_time + timedelta(seconds=30), execution_duration_ms=10.0, ), create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="deny", timestamp=base_time + timedelta(seconds=60), execution_duration_ms=20.0, ), create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=False, timestamp=base_time + timedelta(seconds=90), execution_duration_ms=30.0, @@ -400,7 +419,7 @@ async def test_timeseries_aggregates_events_per_bucket( response = client.get( "/api/v1/observability/stats", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "1h", "include_timeseries": "true", }, @@ -434,12 +453,12 @@ async def test_timeseries_empty_buckets_included( ): """Empty buckets are included with zero counts.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create events only at the start of the time range event = create_test_event( - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, timestamp=now - timedelta(minutes=55), ) @@ -449,7 +468,7 @@ async def test_timeseries_empty_buckets_included( response = client.get( "/api/v1/observability/stats", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "1h", "include_timeseries": "true", }, @@ -484,27 +503,27 @@ class TestControlStats: async def test_control_stats_basic(self, client: TestClient, setup_observability): """Test getting stats for a single control.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" # Create events for multiple controls events = [ - create_test_event(control_id=1, agent_uuid=agent_uuid, matched=True, action="allow"), - create_test_event(control_id=1, agent_uuid=agent_uuid, matched=True, action="deny"), - create_test_event(control_id=2, agent_uuid=agent_uuid, matched=True, action="warn"), + create_test_event(control_id=1, agent_name=agent_name, matched=True, action="allow"), + create_test_event(control_id=1, agent_name=agent_name, matched=True, action="deny"), + create_test_event(control_id=2, agent_name=agent_name, matched=True, action="warn"), ] await store.store(events) # Get stats for control 1 only response = client.get( "/api/v1/observability/stats/controls/1", - params={"agent_uuid": str(agent_uuid), "time_range": "1h"}, + params={"agent_name": str(agent_name), "time_range": "1h"}, ) assert response.status_code == 200 data = response.json() # Verify response structure - assert data["agent_uuid"] == str(agent_uuid) + assert data["agent_name"] == str(agent_name) assert data["control_id"] == 1 assert data["control_name"] == "control-1" assert "stats" in data @@ -520,21 +539,21 @@ async def test_control_stats_basic(self, client: TestClient, setup_observability async def test_control_stats_with_timeseries(self, client: TestClient, setup_observability): """Test control stats with timeseries.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(timezone.utc) # Create events for control 1 at different times events = [ create_test_event( control_id=1, - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="allow", timestamp=now - timedelta(minutes=30), ), create_test_event( control_id=1, - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="deny", timestamp=now - timedelta(minutes=10), @@ -542,7 +561,7 @@ async def test_control_stats_with_timeseries(self, client: TestClient, setup_obs # Control 2 event (should not appear) create_test_event( control_id=2, - agent_uuid=agent_uuid, + agent_name=agent_name, matched=True, action="warn", timestamp=now - timedelta(minutes=20), @@ -553,7 +572,7 @@ async def test_control_stats_with_timeseries(self, client: TestClient, setup_obs response = client.get( "/api/v1/observability/stats/controls/1", params={ - "agent_uuid": str(agent_uuid), + "agent_name": str(agent_name), "time_range": "1h", "include_timeseries": "true", }, @@ -579,16 +598,16 @@ async def test_control_stats_with_timeseries(self, client: TestClient, setup_obs async def test_control_stats_no_data(self, client: TestClient, setup_observability): """Test control stats when control has no events.""" store = setup_observability - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" # Create event for control 1 only - event = create_test_event(control_id=1, agent_uuid=agent_uuid, matched=True) + event = create_test_event(control_id=1, agent_name=agent_name, matched=True) await store.store([event]) # Query for control 2 (no events) response = client.get( "/api/v1/observability/stats/controls/2", - params={"agent_uuid": str(agent_uuid), "time_range": "1h"}, + params={"agent_name": str(agent_name), "time_range": "1h"}, ) assert response.status_code == 200 @@ -647,12 +666,12 @@ def test_query_events_filters_and_pagination(self, client: TestClient, setup_obs def test_get_stats_aggregates_events(self, client: TestClient, setup_observability): """Test GET /stats aggregates events for an agent.""" # Given: events for a specific agent and one other agent - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" event1 = create_test_event(control_id=1, action="allow", matched=True).model_copy( - update={"agent_uuid": agent_uuid} + update={"agent_name": agent_name} ) event2 = create_test_event(control_id=2, action="deny", matched=True).model_copy( - update={"agent_uuid": agent_uuid, "trace_id": "c" * 32} + update={"agent_name": agent_name, "trace_id": "c" * 32} ) # Event from different agent (should not be counted) event3 = create_test_event(control_id=1, action="warn", matched=True).model_copy( @@ -669,7 +688,7 @@ def test_get_stats_aggregates_events(self, client: TestClient, setup_observabili # When: getting stats for the agent resp = client.get( "/api/v1/observability/stats", - params={"agent_uuid": str(agent_uuid), "time_range": "1h"}, + params={"agent_name": str(agent_name), "time_range": "1h"}, ) assert resp.status_code == 200 diff --git a/server/tests/test_observability_models.py b/server/tests/test_observability_models.py index c34f172d..18335c83 100644 --- a/server/tests/test_observability_models.py +++ b/server/tests/test_observability_models.py @@ -26,8 +26,7 @@ def test_valid_event(self): event = ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), - agent_name="test-agent", + agent_name="test-agent", control_id=123, control_name="sql-injection-check", check_stage="pre", @@ -49,7 +48,6 @@ def test_trace_id_validation_empty(self): ControlExecutionEvent( trace_id="", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=123, control_name="test", @@ -67,8 +65,7 @@ def test_trace_id_accepts_various_formats(self): event = ControlExecutionEvent( trace_id="my-custom-trace-id", # non-hex, non-32-char span_id="my-span", - agent_uuid=uuid4(), - agent_name="test-agent", + agent_name="test-agent", control_id=123, control_name="test", check_stage="pre", @@ -86,7 +83,6 @@ def test_span_id_validation_empty(self): ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="", - agent_uuid=uuid4(), agent_name="test-agent", control_id=123, control_name="test", @@ -104,7 +100,6 @@ def test_confidence_bounds(self): ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=123, control_name="test", @@ -121,7 +116,6 @@ def test_check_stage_values(self): ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=123, control_name="test", @@ -138,7 +132,6 @@ def test_action_values(self): event = ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=123, control_name="test", @@ -155,8 +148,7 @@ def test_timestamp_default(self): event = ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), - agent_name="test-agent", + agent_name="test-agent", control_id=123, control_name="test", check_stage="pre", @@ -174,8 +166,7 @@ def test_optional_fields(self): event = ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), - agent_name="test-agent", + agent_name="test-agent", control_id=123, control_name="test", check_stage="pre", @@ -196,12 +187,11 @@ def test_optional_fields(self): def test_to_dict(self): """Test serialization to dict.""" - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" event = ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=agent_uuid, - agent_name="test-agent", + agent_name=agent_name, control_id=123, control_name="test", check_stage="pre", @@ -212,7 +202,7 @@ def test_to_dict(self): ) data = event.to_dict() assert data["trace_id"] == "4bf92f3577b34da6a3ce929d0e0e4736" - assert data["agent_uuid"] == agent_uuid + assert data["agent_name"] == agent_name assert data["matched"] is False @@ -225,7 +215,6 @@ def test_valid_batch(self): ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=i, control_name=f"control-{i}", @@ -251,7 +240,6 @@ def test_max_batch_size(self): ControlExecutionEvent( trace_id="4bf92f3577b34da6a3ce929d0e0e4736", span_id="00f067aa0ba902b7", - agent_uuid=uuid4(), agent_name="test-agent", control_id=i, control_name=f"control-{i}", @@ -353,7 +341,7 @@ class TestStatsRequest: def test_valid_request(self): """Test creating valid stats request.""" request = StatsRequest( - agent_uuid=uuid4(), + agent_name=f"agent-{uuid4().hex[:12]}", time_range="5m", ) assert request.time_range == "5m" @@ -362,11 +350,10 @@ def test_time_range_values(self): """Test valid time range values.""" for time_range in ["1m", "5m", "15m", "1h", "24h", "7d", "30d", "180d", "365d"]: request = StatsRequest( - agent_uuid=uuid4(), + agent_name=f"agent-{uuid4().hex[:12]}", time_range=time_range, ) assert request.time_range == time_range - diff --git a/server/tests/test_observability_store_postgres.py b/server/tests/test_observability_store_postgres.py index 13d50c89..2b53385e 100644 --- a/server/tests/test_observability_store_postgres.py +++ b/server/tests/test_observability_store_postgres.py @@ -21,7 +21,7 @@ def clear_event_table() -> None: def _event( *, - agent_uuid, + agent_name, control_id: int, action: str, matched: bool, @@ -35,8 +35,7 @@ def _event( return ControlExecutionEvent( trace_id=trace_id, span_id=span_id, - agent_uuid=agent_uuid, - agent_name="agent", + agent_name=agent_name, control_id=control_id, control_name=f"control-{control_id}", check_stage=check_stage, @@ -59,12 +58,12 @@ async def test_postgres_event_store_query_events_and_stats() -> None: ) store = PostgresEventStore(session_maker) - agent_uuid = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" now = datetime.now(UTC) events = [ _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=1, action="allow", matched=True, @@ -72,7 +71,7 @@ async def test_postgres_event_store_query_events_and_stats() -> None: trace_id="a" * 32, ), _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=2, action="deny", matched=False, @@ -80,7 +79,7 @@ async def test_postgres_event_store_query_events_and_stats() -> None: trace_id="b" * 32, ), _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=1, action="allow", matched=True, @@ -93,7 +92,7 @@ async def test_postgres_event_store_query_events_and_stats() -> None: await store.store(events) # When: querying events filtered by control_id - query = EventQueryRequest(agent_uuid=agent_uuid, control_ids=[1], limit=10, offset=0) + query = EventQueryRequest(agent_name=agent_name, control_ids=[1], limit=10, offset=0) resp = await store.query_events(query) # Then: only matching events are returned assert resp.total == 2 @@ -107,7 +106,7 @@ async def test_postgres_event_store_query_events_and_stats() -> None: assert all(e.trace_id == "a" * 32 for e in resp.events) # When: querying stats - stats = await store.query_stats(agent_uuid, timedelta(hours=1)) + stats = await store.query_stats(agent_name, timedelta(hours=1)) # Then: totals and action counts are aggregated correctly assert stats.total_executions == 3 assert stats.total_matches == 2 @@ -116,7 +115,7 @@ async def test_postgres_event_store_query_events_and_stats() -> None: assert stats.action_counts == {"allow": 2} # When: querying stats with a control filter - filtered_stats = await store.query_stats(agent_uuid, timedelta(hours=1), control_id=1) + filtered_stats = await store.query_stats(agent_name, timedelta(hours=1), control_id=1) # Then: only the requested control is returned assert len(filtered_stats.stats) == 1 assert filtered_stats.stats[0].control_id == 1 @@ -149,8 +148,8 @@ async def test_postgres_event_store_query_events_all_filters() -> None: ) store = PostgresEventStore(session_maker) - agent_uuid = uuid4() - other_agent = uuid4() + agent_name = f"agent-{uuid4().hex[:12]}" + other_agent = f"agent-{uuid4().hex[:12]}" now = datetime.now(UTC) target_exec_id = "exec-1" @@ -159,7 +158,7 @@ async def test_postgres_event_store_query_events_all_filters() -> None: events = [ _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=1, action="allow", matched=True, @@ -171,7 +170,7 @@ async def test_postgres_event_store_query_events_all_filters() -> None: applies_to="llm_call", ), _event( - agent_uuid=other_agent, + agent_name=other_agent, control_id=2, action="deny", matched=False, @@ -189,7 +188,7 @@ async def test_postgres_event_store_query_events_all_filters() -> None: # When: querying with all supported filters query = EventQueryRequest( control_execution_id=target_exec_id, - agent_uuid=agent_uuid, + agent_name=agent_name, start_time=now - timedelta(seconds=2), end_time=now, trace_id=target_trace_id, @@ -215,8 +214,7 @@ async def test_postgres_event_store_parses_string_json_rows() -> None: event = ControlExecutionEvent( trace_id="a" * 32, span_id="b" * 16, - agent_uuid=uuid4(), - agent_name="agent", + agent_name="agent-test-01", control_id=1, control_name="control-1", check_stage="pre", diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index b5789421..446c520a 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -6,12 +6,12 @@ def _create_agent(client: TestClient, name: str | None = None) -> tuple[str, str]: - """Helper: Create an agent and return (agent_id, agent_name).""" - agent_id = str(uuid.uuid4()) - agent_name = name or f"agent-{uuid.uuid4()}" + """Helper: Create an agent and return (agent_name, agent_name).""" + agent_name = (name or f"agent-{uuid.uuid4().hex[:12]}").lower() + if len(agent_name) < 10: + agent_name = f"{agent_name}-agent".replace("--", "-") payload = { "agent": { - "agent_id": agent_id, "agent_name": agent_name, "agent_description": "test", "agent_version": "1.0", @@ -21,7 +21,7 @@ def _create_agent(client: TestClient, name: str | None = None) -> tuple[str, str } resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - return agent_id, agent_name + return agent_name, agent_name def _create_policy(client: TestClient, name: str | None = None) -> int: @@ -281,8 +281,8 @@ def test_control_shared_between_policies(client: TestClient) -> None: assert control_id in resp_b.json()["control_ids"] # And: Agents with either policy see the control - agent_a_id, _ = _create_agent(client, "agent-a") - agent_b_id, _ = _create_agent(client, "agent-b") + agent_a_id, _ = _create_agent(client, "agent-alpha-01") + agent_b_id, _ = _create_agent(client, "agent-beta-02") client.post(f"/api/v1/agents/{agent_a_id}/policy/{policy_a_id}") client.post(f"/api/v1/agents/{agent_b_id}/policy/{policy_b_id}") diff --git a/server/tests/test_services_controls.py b/server/tests/test_services_controls.py index e4d26670..797835d2 100644 --- a/server/tests/test_services_controls.py +++ b/server/tests/test_services_controls.py @@ -46,7 +46,6 @@ async def test_list_controls_for_agent_returns_controls(async_db) -> None: policy = Policy(name=f"policy-{uuid.uuid4()}") control = Control(name=f"control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) agent = Agent( - agent_uuid=uuid.uuid4(), name=f"agent-{uuid.uuid4()}", data={}, policy=policy, @@ -60,7 +59,7 @@ async def test_list_controls_for_agent_returns_controls(async_db) -> None: await async_db.commit() # When: listing controls for the agent - controls = await list_controls_for_agent(agent.agent_uuid, async_db) + controls = await list_controls_for_agent(agent.name, async_db) # Then: the API control is returned with expected fields assert len(controls) == 1 @@ -74,7 +73,6 @@ async def test_list_controls_for_agent_corrupted_data_raises(async_db) -> None: policy = Policy(name=f"policy-{uuid.uuid4()}") control = Control(name=f"control-{uuid.uuid4()}", data={"bad": "data"}) agent = Agent( - agent_uuid=uuid.uuid4(), name=f"agent-{uuid.uuid4()}", data={}, policy=policy, @@ -89,7 +87,7 @@ async def test_list_controls_for_agent_corrupted_data_raises(async_db) -> None: # When: listing controls for the agent with pytest.raises(APIValidationError) as exc_info: - await list_controls_for_agent(agent.agent_uuid, async_db) + await list_controls_for_agent(agent.name, async_db) # Then: corrupted data error is raised assert exc_info.value.error_code == ErrorCode.CORRUPTED_DATA diff --git a/server/tests/utils.py b/server/tests/utils.py index 73efa678..a2a32098 100644 --- a/server/tests/utils.py +++ b/server/tests/utils.py @@ -18,8 +18,8 @@ def create_and_assign_policy( client: TestClient, control_config: dict[str, Any] | None = None, - agent_name: str = "MyTestAgent", -) -> tuple[uuid.UUID, str]: + agent_name: str = "mytestagent01", +) -> tuple[str, str]: """Helper to setup Agent -> Policy -> Control hierarchy. Args: @@ -28,7 +28,7 @@ def create_and_assign_policy( agent_name: Name for the test agent Returns: - tuple: (agent_uuid, control_name) + tuple: (agent_name, control_name) """ if control_config is None: control_config = VALID_CONTROL_PAYLOAD.copy() @@ -54,18 +54,19 @@ def create_and_assign_policy( assert resp.status_code == 200 # 5. Register Agent - agent_uuid = uuid.uuid4() + normalized_agent_name = agent_name.lower() + if len(normalized_agent_name) < 10: + normalized_agent_name = f"{normalized_agent_name}-agent".replace("--", "-") resp = client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": str(agent_uuid), - "agent_name": agent_name + "agent_name": normalized_agent_name }, "steps": [] }) assert resp.status_code == 200 # 6. Assign Policy to Agent - resp = client.post(f"/api/v1/agents/{str(agent_uuid)}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{normalized_agent_name}/policy/{policy_id}") assert resp.status_code == 200 - return agent_uuid, control_name + return normalized_agent_name, control_name diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index c72911a9..3facbcb0 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -55,27 +55,30 @@ export const api = { apiClient.GET('/api/v1/agents', { params: { query: params }, }), - get: (agentId: GetAgentPathParams['agent_id']) => - apiClient.GET('/api/v1/agents/{agent_id}', { - params: { path: { agent_id: agentId } }, + get: (agentName: GetAgentPathParams['agent_name']) => + apiClient.GET('/api/v1/agents/{agent_name}', { + params: { path: { agent_name: agentName } }, }), initAgent: (data: InitAgentRequestBody) => apiClient.POST('/api/v1/agents/initAgent', { body: data }), - getControls: (agentId: GetAgentControlsPathParams['agent_id']) => - apiClient.GET('/api/v1/agents/{agent_id}/controls', { - params: { path: { agent_id: agentId } }, + getControls: (agentName: GetAgentControlsPathParams['agent_name']) => + apiClient.GET('/api/v1/agents/{agent_name}/controls', { + params: { path: { agent_name: agentName } }, }), - setPolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => - apiClient.POST('/api/v1/agents/{agent_id}/policy/{policy_id}', { - params: { path: { agent_id: agentId, policy_id: policyId } }, + setPolicy: ( + agentName: GetAgentPathParams['agent_name'], + policyId: number + ) => + apiClient.POST('/api/v1/agents/{agent_name}/policy/{policy_id}', { + params: { path: { agent_name: agentName, policy_id: policyId } }, }), - getPolicy: (agentId: GetAgentPathParams['agent_id']) => - apiClient.GET('/api/v1/agents/{agent_id}/policy', { - params: { path: { agent_id: agentId } }, + getPolicy: (agentName: GetAgentPathParams['agent_name']) => + apiClient.GET('/api/v1/agents/{agent_name}/policy', { + params: { path: { agent_name: agentName } }, }), - deletePolicy: (agentId: GetAgentPathParams['agent_id']) => - apiClient.DELETE('/api/v1/agents/{agent_id}/policy', { - params: { path: { agent_id: agentId } }, + deletePolicy: (agentName: GetAgentPathParams['agent_name']) => + apiClient.DELETE('/api/v1/agents/{agent_name}/policy', { + params: { path: { agent_name: agentName } }, }), }, evaluators: { @@ -143,7 +146,7 @@ export const api = { }, observability: { getStats: (params: { - agent_uuid: string; + agent_name: string; time_range?: | '1m' | '5m' diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index c1e36591..8f635916 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -15,11 +15,11 @@ export interface paths { * List all agents * @description List all registered agents with cursor-based pagination. * - * Returns a summary of each agent including ID, name, policy assignment, + * Returns a summary of each agent including identifier, policy assignment, * and counts of registered steps and evaluators. * * Args: - * cursor: Optional cursor for pagination (UUID of last agent from previous page) + * cursor: Optional cursor for pagination (last agent name from previous page) * limit: Pagination limit (default 20, max 100) * name: Optional name filter (case-insensitive partial match) * db: Database session (injected) @@ -51,11 +51,11 @@ export interface paths { * * This endpoint is idempotent: * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists with the same UUID, updates step schemas - * - If the agent name exists with a different UUID, returns 409 Conflict + * - If the agent name exists, updates registration data in place * - * Step versioning: When step schemas change (input_schema or output_schema), - * a new version is created automatically. + * conflict_mode controls registration conflict handling: + * - strict (default): preserve compatibility checks and conflict errors + * - overwrite: latest init payload replaces steps/evaluators and returns change summary * * Args: * request: Agent metadata and step schemas @@ -63,10 +63,6 @@ export interface paths { * * Returns: * InitAgentResponse with created flag and active controls (if policy assigned) - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update */ post: operations['init_agent_api_v1_agents_initAgent_post']; delete?: never; @@ -75,7 +71,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}': { + '/api/v1/agents/{agent_name}': { parameters: { query?: never; header?: never; @@ -89,7 +85,7 @@ export interface paths { * Returns the latest version of each step (deduplicated by type+name). * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -99,7 +95,7 @@ export interface paths { * HTTPException 404: Agent not found * HTTPException 422: Agent data is corrupted */ - get: operations['get_agent_api_v1_agents__agent_id__get']; + get: operations['get_agent_api_v1_agents__agent_name__get']; put?: never; post?: never; delete?: never; @@ -113,7 +109,7 @@ export interface paths { * Removals are idempotent - attempting to remove non-existent items is not an error. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * request: Lists of step/evaluator identifiers to remove * db: Database session (injected) * @@ -124,91 +120,10 @@ export interface paths { * HTTPException 404: Agent not found * HTTPException 500: Database error during update */ - patch: operations['patch_agent_api_v1_agents__agent_id__patch']; - trace?: never; - }; - '/api/v1/agents/{agent_id}/policy/{policy_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign policy to agent - * @description Assign a policy to an agent, replacing any existing policy assignment. - * - * The agent will immediately inherit all controls from the assigned policy. - * - * Args: - * agent_id: UUID of the agent - * policy_id: ID of the policy to assign - * db: Database session (injected) - * - * Returns: - * SetPolicyResponse with success flag and previous policy ID (if any) - * - * Raises: - * HTTPException 404: Agent or policy not found - * HTTPException 500: Database error during assignment - */ - post: operations['set_agent_policy_api_v1_agents__agent_id__policy__policy_id__post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_id}/policy': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent's assigned policy - * @description Retrieve the policy currently assigned to an agent. - * - * Args: - * agent_id: UUID of the agent - * db: Database session (injected) - * - * Returns: - * GetPolicyResponse with policy ID - * - * Raises: - * HTTPException 404: Agent not found or agent has no policy assigned - */ - get: operations['get_agent_policy_api_v1_agents__agent_id__policy_get']; - put?: never; - post?: never; - /** - * Remove agent's policy assignment - * @description Remove the policy assignment from an agent. - * - * The agent will no longer have any protection controls active. - * - * Args: - * agent_id: UUID of the agent - * db: Database session (injected) - * - * Returns: - * DeletePolicyResponse with success flag - * - * Raises: - * HTTPException 404: Agent not found or agent has no policy assigned - * HTTPException 500: Database error during removal - */ - delete: operations['delete_agent_policy_api_v1_agents__agent_id__policy_delete']; - options?: never; - head?: never; - patch?: never; + patch: operations['patch_agent_api_v1_agents__agent_name__patch']; trace?: never; }; - '/api/v1/agents/{agent_id}/controls': { + '/api/v1/agents/{agent_name}/controls': { parameters: { query?: never; header?: never; @@ -223,7 +138,7 @@ export interface paths { * Returns an empty list if the agent has no policy. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * db: Database session (injected) * * Returns: @@ -232,7 +147,7 @@ export interface paths { * Raises: * HTTPException 404: Agent not found */ - get: operations['list_agent_controls_api_v1_agents__agent_id__controls_get']; + get: operations['list_agent_controls_api_v1_agents__agent_name__controls_get']; put?: never; post?: never; delete?: never; @@ -241,7 +156,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}/evaluators': { + '/api/v1/agents/{agent_name}/evaluators': { parameters: { query?: never; header?: never; @@ -257,7 +172,7 @@ export interface paths { * - UI to display available config options * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * cursor: Optional cursor for pagination (name of last evaluator from previous page) * limit: Pagination limit (default 20, max 100) * db: Database session (injected) @@ -268,7 +183,7 @@ export interface paths { * Raises: * HTTPException 404: Agent not found */ - get: operations['list_agent_evaluators_api_v1_agents__agent_id__evaluators_get']; + get: operations['list_agent_evaluators_api_v1_agents__agent_name__evaluators_get']; put?: never; post?: never; delete?: never; @@ -277,7 +192,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}/evaluators/{evaluator_name}': { + '/api/v1/agents/{agent_name}/evaluators/{evaluator_name}': { parameters: { query?: never; header?: never; @@ -289,7 +204,7 @@ export interface paths { * @description Get a specific evaluator schema registered with an agent. * * Args: - * agent_id: UUID of the agent + * agent_name: Agent identifier * evaluator_name: Name of the evaluator * db: Database session (injected) * @@ -299,7 +214,7 @@ export interface paths { * Raises: * HTTPException 404: Agent or evaluator not found */ - get: operations['get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get']; + get: operations['get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get']; put?: never; post?: never; delete?: never; @@ -308,118 +223,81 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/policies': { + '/api/v1/agents/{agent_name}/policy': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; /** - * Create a new policy - * @description Create a new empty policy with a unique name. - * - * Policies contain controls and can be assigned to agents. - * A newly created policy has no controls until they are explicitly added. + * Get agent's assigned policy + * @description Retrieve the policy currently assigned to an agent. * * Args: - * request: Policy creation request with unique name + * agent_name: Agent identifier * db: Database session (injected) * * Returns: - * CreatePolicyResponse with the new policy's ID + * GetPolicyResponse with policy ID * * Raises: - * HTTPException 409: Policy with this name already exists - * HTTPException 500: Database error during creation + * HTTPException 404: Agent not found or agent has no policy assigned */ - put: operations['create_policy_api_v1_policies_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies/{policy_id}/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; + get: operations['get_agent_policy_api_v1_agents__agent_name__policy_get']; put?: never; + post?: never; /** - * Add control to policy - * @description Associate a control with a policy. - * - * This operation is idempotent - adding the same control multiple times has no effect. - * Agents with this policy will immediately see the added control. - * - * Args: - * policy_id: ID of the policy - * control_id: ID of the control to add - * db: Database session (injected) - * - * Returns: - * AssocResponse with success flag - * - * Raises: - * HTTPException 404: Policy or control not found - * HTTPException 500: Database error - */ - post: operations['add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post']; - /** - * Remove control from policy - * @description Remove a control from a policy. + * Remove agent's policy assignment + * @description Remove the policy assignment from an agent. * - * This operation is idempotent - removing a non-associated control has no effect. - * Agents with this policy will immediately lose the removed control. + * The agent will no longer have any protection controls active. * * Args: - * policy_id: ID of the policy - * control_id: ID of the control to remove + * agent_name: Agent identifier * db: Database session (injected) * * Returns: - * AssocResponse with success flag + * DeletePolicyResponse with success flag * * Raises: - * HTTPException 404: Policy or control not found - * HTTPException 500: Database error + * HTTPException 404: Agent not found or agent has no policy assigned + * HTTPException 500: Database error during removal */ - delete: operations['remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete']; + delete: operations['delete_agent_policy_api_v1_agents__agent_name__policy_delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/policies/{policy_id}/controls': { + '/api/v1/agents/{agent_name}/policy/{policy_id}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; + put?: never; /** - * List policy's controls - * @description List all controls associated with a policy. + * Assign policy to agent + * @description Assign a policy to an agent, replacing any existing policy assignment. + * + * The agent will immediately inherit all controls from the assigned policy. * * Args: - * policy_id: ID of the policy + * agent_name: Agent identifier + * policy_id: ID of the policy to assign * db: Database session (injected) * * Returns: - * GetPolicyControlsResponse with list of control IDs + * SetPolicyResponse with success flag and previous policy ID (if any) * * Raises: - * HTTPException 404: Policy not found + * HTTPException 404: Agent or policy not found + * HTTPException 500: Database error during assignment */ - get: operations['list_policy_controls_api_v1_policies__policy_id__controls_get']; - put?: never; - post?: never; + post: operations['set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post']; delete?: never; options?: never; head?: never; @@ -483,6 +361,33 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/controls/validate': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Validate control configuration + * @description Validate control configuration data without saving it. + * + * Args: + * request: Control configuration data to validate + * db: Database session (injected) + * + * Returns: + * ValidateControlDataResponse with success=True if valid + */ + post: operations['validate_control_data_api_v1_controls_validate_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/controls/{control_id}': { parameters: { query?: never; @@ -607,6 +512,36 @@ export interface paths { patch?: never; trace?: never; }; + '/api/v1/evaluation': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Analyze content safety + * @description Analyze content for safety and control violations. + * + * Runs all controls assigned to the agent via policy through the + * evaluation engine. Controls are evaluated in parallel with + * cancel-on-deny for efficiency. + * + * Custom evaluators must be deployed as Evaluator classes + * with the engine. Their schemas are registered via initAgent. + * + * Optionally accepts X-Trace-Id and X-Span-Id headers for + * OpenTelemetry-compatible distributed tracing. + */ + post: operations['evaluate_api_v1_evaluation_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/v1/evaluator-configs': { parameters: { query?: never; @@ -644,36 +579,6 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/evaluation': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Analyze content safety - * @description Analyze content for safety and control violations. - * - * Runs all controls assigned to the agent via policy through the - * evaluation engine. Controls are evaluated in parallel with - * cancel-on-deny for efficiency. - * - * Custom evaluators must be deployed as Evaluator classes - * with the engine. Their schemas are registered via initAgent. - * - * Optionally accepts X-Trace-Id and X-Span-Id headers for - * OpenTelemetry-compatible distributed tracing. - */ - post: operations['evaluate_api_v1_evaluation_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/api/v1/evaluators': { parameters: { query?: never; @@ -694,7 +599,7 @@ export interface paths { * - **sql**: SQL query validation * * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. */ get: operations['get_evaluators_api_v1_evaluators_get']; put?: never; @@ -751,7 +656,7 @@ export interface paths { * - trace_id: Get all events for a request * - span_id: Get all events for a function call * - control_execution_id: Get a specific event - * - agent_uuid: Filter by agent + * - agent_name: Filter by agent * - control_ids: Filter by controls * - actions: Filter by actions (allow, deny, warn, log) * - matched: Filter by matched status @@ -790,7 +695,7 @@ export interface paths { * Use /stats/controls/{control_id} for single control stats. * * Args: - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) @@ -822,7 +727,7 @@ export interface paths { * * Args: * control_id: Control ID to get stats for - * agent_uuid: Agent to get stats for + * agent_name: Agent to get stats for * time_range: Time range (1m, 5m, 15m, 1h, 24h, 7d, 30d, 180d, 365d) * include_timeseries: Include time-series data points for trend visualization * store: Event store (injected) @@ -861,22 +766,140 @@ export interface paths { patch?: never; trace?: never; }; - '/health': { + '/api/v1/policies': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + get?: never; /** - * Health check - * @description Check if the server is running and responsive. + * Create a new policy + * @description Create a new empty policy with a unique name. * - * This endpoint does not check database connectivity. + * Policies contain controls and can be assigned to agents. + * A newly created policy has no controls until they are explicitly added. * - * Returns: - * HealthResponse with status and version - */ + * Args: + * request: Policy creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreatePolicyResponse with the new policy's ID + * + * Raises: + * HTTPException 409: Policy with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations['create_policy_api_v1_policies_put']; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/policies/{policy_id}/controls': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policy's controls + * @description List all controls associated with a policy. + * + * Args: + * policy_id: ID of the policy + * db: Database session (injected) + * + * Returns: + * GetPolicyControlsResponse with list of control IDs + * + * Raises: + * HTTPException 404: Policy not found + */ + get: operations['list_policy_controls_api_v1_policies__policy_id__controls_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/policies/{policy_id}/controls/{control_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add control to policy + * @description Associate a control with a policy. + * + * This operation is idempotent - adding the same control multiple times has no effect. + * Agents with this policy will immediately see the added control. + * + * Args: + * policy_id: ID of the policy + * control_id: ID of the control to add + * db: Database session (injected) + * + * Returns: + * AssocResponse with success flag + * + * Raises: + * HTTPException 404: Policy or control not found + * HTTPException 500: Database error + */ + post: operations['add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post']; + /** + * Remove control from policy + * @description Remove a control from a policy. + * + * This operation is idempotent - removing a non-associated control has no effect. + * Agents with this policy will immediately lose the removed control. + * + * Args: + * policy_id: ID of the policy + * control_id: ID of the control to remove + * db: Database session (injected) + * + * Returns: + * AssocResponse with success flag + * + * Raises: + * HTTPException 404: Policy or control not found + * HTTPException 500: Database error + */ + delete: operations['remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/health': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Check if the server is running and responsive. + * + * This endpoint does not check database connectivity. + * + * Returns: + * HealthResponse with status and version + */ get: operations['health_check_health_get']; put?: never; post?: never; @@ -895,10 +918,9 @@ export interface components { * @description Agent metadata for registration and tracking. * * An agent represents an AI system that can be protected and monitored. - * Each agent has a unique ID and can have multiple steps registered with it. + * Each agent has a unique immutable name and can have multiple steps registered with it. * @example { * "agent_description": "Handles customer inquiries and support tickets", - * "agent_id": "550e8400-e29b-41d4-a716-446655440000", * "agent_metadata": { * "environment": "production", * "team": "support" @@ -909,26 +931,27 @@ export interface components { */ Agent: { /** - * Agent Id - * Format: uuid - * @description Unique identifier for the agent (UUID format) - */ - agent_id: string; - /** - * Agent Name - * @description Human-readable name for the agent + * Agent Created At + * @description ISO 8601 timestamp when agent was created */ - agent_name: string; + agent_created_at?: string | null; /** * Agent Description * @description Optional description of the agent's purpose */ agent_description?: string | null; /** - * Agent Created At - * @description ISO 8601 timestamp when agent was created + * Agent Metadata + * @description Free-form metadata dictionary for custom properties */ - agent_created_at?: string | null; + agent_metadata?: { + [key: string]: unknown; + } | null; + /** + * Agent Name + * @description Unique immutable identifier for the agent + */ + agent_name: string; /** * Agent Updated At * @description ISO 8601 timestamp when agent was last updated @@ -939,13 +962,6 @@ export interface components { * @description Semantic version string (e.g. '1.0.0') */ agent_version?: string | null; - /** - * Agent Metadata - * @description Free-form metadata dictionary for custom properties - */ - agent_metadata?: { - [key: string]: unknown; - } | null; }; /** AgentControlsResponse */ AgentControlsResponse: { @@ -960,14 +976,9 @@ export interface components { * @description Reference to an agent (for listing which agents use a control). */ AgentRef: { - /** - * Agent Id - * @description Agent UUID - */ - agent_id: string; /** * Agent Name - * @description Agent name + * @description Agent identifier */ agent_name: string; }; @@ -977,31 +988,21 @@ export interface components { */ AgentSummary: { /** - * Agent Id - * @description UUID of the agent + * Active Controls Count + * @description Number of active controls from agent's policy + * @default 0 */ - agent_id: string; + active_controls_count: number; /** * Agent Name - * @description Human-readable name of the agent + * @description Unique identifier of the agent */ agent_name: string; - /** - * Policy Id - * @description ID of assigned policy, if any - */ - policy_id?: number | null; /** * Created At * @description ISO 8601 timestamp when agent was created */ created_at?: string | null; - /** - * Step Count - * @description Number of steps registered with the agent - * @default 0 - */ - step_count: number; /** * Evaluator Count * @description Number of evaluators registered with the agent @@ -1009,11 +1010,16 @@ export interface components { */ evaluator_count: number; /** - * Active Controls Count - * @description Number of active controls from agent's policy + * Policy Id + * @description ID of assigned policy, if any + */ + policy_id?: number | null; + /** + * Step Count + * @description Number of steps registered with the agent * @default 0 */ - active_controls_count: number; + step_count: number; }; /** AssocResponse */ AssocResponse: { @@ -1037,7 +1043,6 @@ export interface components { * { * "action": "deny", * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", * "applies_to": "llm_call", * "check_stage": "pre", * "confidence": 0.95, @@ -1069,20 +1074,20 @@ export interface components { */ BatchEventsResponse: { /** - * Received - * @description Number of events received + * Dropped + * @description Number of events dropped */ - received: number; + dropped: number; /** * Enqueued * @description Number of events enqueued */ enqueued: number; /** - * Dropped - * @description Number of events dropped + * Received + * @description Number of events received */ - dropped: number; + received: number; /** * Status * @description Overall ingestion status @@ -1090,6 +1095,15 @@ export interface components { */ status: 'queued' | 'partial' | 'failed'; }; + /** + * ConflictMode + * @description Conflict handling mode for initAgent registration updates. + * + * STRICT preserves compatibility checks and raises conflicts on incompatible changes. + * OVERWRITE applies latest-init-wins replacement for steps and evaluators. + * @enum {string} + */ + ConflictMode: 'strict' | 'overwrite'; /** * Control * @description A control with identity and configuration. @@ -1098,11 +1112,11 @@ export interface components { * are returned from API endpoints. Unconfigured controls are filtered out. */ Control: { + control: components['schemas']['ControlDefinition']; /** Id */ id: number; /** Name */ name: string; - control: components['schemas']['ControlDefinition']; }; /** * ControlAction @@ -1153,6 +1167,8 @@ export interface components { * } */ ControlDefinition: { + /** @description What action to take when control matches */ + action: components['schemas']['ControlAction']; /** * Description * @description Detailed description of the control @@ -1164,6 +1180,8 @@ export interface components { * @default true */ enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components['schemas']['EvaluatorSpec']; /** * Execution * @description Where this control executes @@ -1174,10 +1192,6 @@ export interface components { scope?: components['schemas']['ControlScope']; /** @description What data to select from the payload */ selector: components['schemas']['ControlSelector']; - /** @description How to evaluate the selected data */ - evaluator: components['schemas']['EvaluatorConfig']; - /** @description What action to take when control matches */ - action: components['schemas']['ControlAction']; /** * Tags * @description Tags for categorization @@ -1199,12 +1213,9 @@ export interface components { * control_execution_id: Unique ID for this specific control execution * trace_id: OpenTelemetry-compatible trace ID (128-bit hex, 32 chars) * span_id: OpenTelemetry-compatible span ID (64-bit hex, 16 chars) - * agent_uuid: UUID of the agent that executed the control - * agent_name: Name of the agent (denormalized for queries) + * agent_name: Identifier of the agent that executed the control * control_id: Database ID of the control * control_name: Name of the control (denormalized for queries) - * control_set_id: Optional ID of the control set - * control_set_name: Optional name of the control set * check_stage: "pre" (before execution) or "post" (after execution) * applies_to: "llm_call" or "tool_call" * action: The action taken (allow, deny, warn, log) @@ -1219,7 +1230,6 @@ export interface components { * @example { * "action": "deny", * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", * "applies_to": "llm_call", * "check_stage": "pre", * "confidence": 0.95, @@ -1237,31 +1247,38 @@ export interface components { */ ControlExecutionEvent: { /** - * Control Execution Id - * @description Unique ID for this control execution + * Action + * @description Action taken by the control + * @enum {string} */ - control_execution_id?: string; + action: 'allow' | 'deny' | 'warn' | 'log'; /** - * Trace Id - * @description Trace ID for distributed tracing (SDK generates OTEL-compatible 32-char hex) + * Agent Name + * @description Identifier of the agent */ - trace_id: string; + agent_name: string; /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} */ - span_id: string; + applies_to: 'llm_call' | 'tool_call'; /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} */ - agent_uuid: string; + check_stage: 'pre' | 'post'; /** - * Agent Name - * @description Name of the agent (denormalized) + * Confidence + * @description Confidence score (0.0 to 1.0) */ - agent_name: string; + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; /** * Control Id * @description Database ID of the control @@ -1273,72 +1290,65 @@ export interface components { */ control_name: string; /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} + * Error Message + * @description Error message if evaluation failed */ - check_stage: 'pre' | 'post'; + error_message?: string | null; /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} + * Evaluator Name + * @description Name of the evaluator used */ - applies_to: 'llm_call' | 'tool_call'; + evaluator_name?: string | null; /** - * Action - * @description Action taken by the control - * @enum {string} + * Execution Duration Ms + * @description Execution duration in milliseconds */ - action: 'allow' | 'deny' | 'warn' | 'log'; + execution_duration_ms?: number | null; /** * Matched * @description Whether the evaluator matched (True) or not (False) */ matched: boolean; /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Timestamp - * Format: date-time - * @description When the control was executed (UTC) - */ - timestamp?: string; - /** - * Execution Duration Ms - * @description Execution duration in milliseconds - */ - execution_duration_ms?: number | null; - /** - * Evaluator Name - * @description Name of the evaluator used + * Metadata + * @description Additional metadata */ - evaluator_name?: string | null; + metadata?: { + [key: string]: unknown; + }; /** * Selector Path * @description Selector path used to extract data */ selector_path?: string | null; /** - * Error Message - * @description Error message if evaluation failed + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) */ - error_message?: string | null; + span_id: string; /** - * Metadata - * @description Additional metadata + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) */ - metadata?: { - [key: string]: unknown; - }; + timestamp?: string; + /** + * Trace Id + * @description Trace ID for distributed tracing (SDK generates OTEL-compatible 32-char hex) + */ + trace_id: string; }; /** * ControlMatch * @description Represents a control evaluation result (match, non-match, or error). */ ControlMatch: { + /** + * Action + * @description Action configured for this control + * @enum {string} + */ + action: 'allow' | 'deny' | 'warn' | 'log'; /** * Control Execution Id * @description Unique ID for this control execution (generated by engine) @@ -1354,12 +1364,6 @@ export interface components { * @description Name of the control */ control_name: string; - /** - * Action - * @description Action configured for this control - * @enum {string} - */ - action: 'allow' | 'deny' | 'warn' | 'log'; /** @description Evaluator result (confidence, message, metadata) */ result: components['schemas']['EvaluatorResult']; }; @@ -1394,25 +1398,25 @@ export interface components { */ ControlScope: { /** - * Step Types - * @description Step types this control applies to (omit to apply to all types). Built-in types are 'tool' and 'llm'. - */ - step_types?: string[] | null; - /** - * Step Names - * @description Exact step names this control applies to + * Stages + * @description Evaluation stages this control applies to */ - step_names?: string[] | null; + stages?: ('pre' | 'post')[] | null; /** * Step Name Regex * @description RE2 pattern matched with search() against step name */ step_name_regex?: string | null; /** - * Stages - * @description Evaluation stages this control applies to + * Step Names + * @description Exact step names this control applies to */ - stages?: ('pre' | 'post')[] | null; + step_names?: string[] | null; + /** + * Step Types + * @description Step types this control applies to (omit to apply to all types). Built-in types are 'tool' and 'llm'. + */ + step_types?: string[] | null; }; /** * ControlSelector @@ -1466,6 +1470,21 @@ export interface components { * avg_duration_ms: Average execution duration in milliseconds */ ControlStats: { + /** + * Allow Count + * @description Allow actions + */ + allow_count: number; + /** + * Avg Confidence + * @description Average confidence + */ + avg_confidence: number; + /** + * Avg Duration Ms + * @description Average duration (ms) + */ + avg_duration_ms?: number | null; /** * Control Id * @description Control ID @@ -1476,11 +1495,26 @@ export interface components { * @description Control name */ control_name: string; + /** + * Deny Count + * @description Deny actions + */ + deny_count: number; + /** + * Error Count + * @description Evaluation errors + */ + error_count: number; /** * Execution Count * @description Total executions */ execution_count: number; + /** + * Log Count + * @description Log actions + */ + log_count: number; /** * Match Count * @description Total matches @@ -1491,41 +1525,11 @@ export interface components { * @description Total non-matches */ non_match_count: number; - /** - * Allow Count - * @description Allow actions - */ - allow_count: number; - /** - * Deny Count - * @description Deny actions - */ - deny_count: number; /** * Warn Count * @description Warn actions */ warn_count: number; - /** - * Log Count - * @description Log actions - */ - log_count: number; - /** - * Error Count - * @description Evaluation errors - */ - error_count: number; - /** - * Avg Confidence - * @description Average confidence - */ - avg_confidence: number; - /** - * Avg Duration Ms - * @description Average duration (ms) - */ - avg_duration_ms?: number | null; }; /** * ControlStatsResponse @@ -1534,7 +1538,7 @@ export interface components { * Contains stats for a single control (with optional timeseries). * * Attributes: - * agent_uuid: Agent UUID + * agent_name: Agent identifier * time_range: Time range used * control_id: Control ID * control_name: Control name @@ -1542,16 +1546,10 @@ export interface components { */ ControlStatsResponse: { /** - * Agent Uuid - * Format: uuid - * @description Agent UUID - */ - agent_uuid: string; - /** - * Time Range - * @description Time range used + * Agent Name + * @description Agent identifier */ - time_range: string; + agent_name: string; /** * Control Id * @description Control ID @@ -1564,22 +1562,17 @@ export interface components { control_name: string; /** @description Control statistics */ stats: components['schemas']['StatsTotals']; + /** + * Time Range + * @description Time range used + */ + time_range: string; }; /** * ControlSummary * @description Summary of a control for list responses. */ ControlSummary: { - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; /** * Description * @description Control description @@ -1597,15 +1590,25 @@ export interface components { */ execution?: string | null; /** - * Step Types - * @description Step types in scope + * Id + * @description Control ID */ - step_types?: string[] | null; + id: number; + /** + * Name + * @description Control name + */ + name: string; /** * Stages * @description Evaluation stages in scope */ - stages?: string[] | null; + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; /** * Tags * @description Control tags @@ -1636,10 +1639,12 @@ export interface components { */ CreateEvaluatorConfigRequest: { /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + * Config + * @description Evaluator-specific configuration */ - name: string; + config: { + [key: string]: unknown; + }; /** * Description * @description Optional description @@ -1651,12 +1656,10 @@ export interface components { */ evaluator: string; /** - * Config - * @description Evaluator-specific configuration + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) */ - config: { - [key: string]: unknown; - }; + name: string; }; /** CreatePolicyRequest */ CreatePolicyRequest: { @@ -1679,16 +1682,16 @@ export interface components { * @description Response for deleting a control. */ DeleteControlResponse: { - /** - * Success - * @description Whether the control was deleted - */ - success: boolean; /** * Dissociated From * @description Policy IDs the control was removed from before deletion */ dissociated_from?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; }; /** * DeleteEvaluatorConfigResponse @@ -1717,11 +1720,11 @@ export interface components { * policy compliance, and control rules. * * Attributes: - * agent_uuid: UUID of the agent making the request + * agent_name: Unique identifier of the agent making the request * step: Step payload for evaluation * stage: 'pre' (before execution) or 'post' (after execution) * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "pre", * "step": { * "context": { @@ -1734,7 +1737,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "post", * "step": { * "context": { @@ -1748,7 +1751,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "pre", * "step": { * "context": { @@ -1762,7 +1765,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "post", * "step": { * "context": { @@ -1781,19 +1784,18 @@ export interface components { */ EvaluationRequest: { /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent making the evaluation request + * Agent Name + * @description Identifier of the agent making the evaluation request */ - agent_uuid: string; - /** @description Agent step payload to evaluate */ - step: components['schemas']['Step']; + agent_name: string; /** * Stage * @description Evaluation stage: 'pre' or 'post' * @enum {string} */ stage: 'pre' | 'post'; + /** @description Agent step payload to evaluate */ + step: components['schemas']['Step']; }; /** * EvaluationResponse @@ -1811,70 +1813,36 @@ export interface components { * non_matches: List of controls that were evaluated but did not match (if any) */ EvaluationResponse: { - /** - * Is Safe - * @description Whether content is safe - */ - is_safe: boolean; /** * Confidence * @description Confidence score (0.0 to 1.0) */ confidence: number; /** - * Reason - * @description Explanation for the decision + * Errors + * @description List of controls that failed during evaluation (if any) */ - reason?: string | null; + errors?: components['schemas']['ControlMatch'][] | null; + /** + * Is Safe + * @description Whether content is safe + */ + is_safe: boolean; /** * Matches * @description List of controls that matched/triggered (if any) */ matches?: components['schemas']['ControlMatch'][] | null; - /** - * Errors - * @description List of controls that failed during evaluation (if any) - */ - errors?: components['schemas']['ControlMatch'][] | null; /** * Non Matches * @description List of controls that were evaluated but did not match (if any) */ non_matches?: components['schemas']['ControlMatch'][] | null; - }; - /** - * EvaluatorConfig - * @description Evaluator configuration. See GET /evaluators for available evaluators and schemas. - * - * Evaluator reference formats: - * - Built-in: "regex", "list" - * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) - */ - EvaluatorConfig: { - /** - * Name - * @description Evaluator name or agent-scoped reference (agent:evaluator) - * @example regex - * @example list - * @example my-agent:pii-detector - */ - name: string; /** - * Config - * @description Evaluator-specific configuration - * @example { - * "pattern": "\\d{3}-\\d{2}-\\d{4}" - * } - * @example { - * "logic": "any", - * "values": [ - * "admin" - * ] - * } + * Reason + * @description Explanation for the decision */ - config: { - [key: string]: unknown; - }; + reason?: string | null; }; /** * EvaluatorConfigItem @@ -1882,15 +1850,17 @@ export interface components { */ EvaluatorConfigItem: { /** - * Id - * @description Evaluator config ID + * Config + * @description Evaluator-specific configuration */ - id: number; + config: { + [key: string]: unknown; + }; /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + * Created At + * @description ISO 8601 created timestamp */ - name: string; + created_at?: string | null; /** * Description * @description Optional description @@ -1902,17 +1872,15 @@ export interface components { */ evaluator: string; /** - * Config - * @description Evaluator-specific configuration + * Id + * @description Evaluator config ID */ - config: { - [key: string]: unknown; - }; + id: number; /** - * Created At - * @description ISO 8601 created timestamp + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) */ - created_at?: string | null; + name: string; /** * Updated At * @description ISO 8601 updated timestamp @@ -1925,20 +1893,22 @@ export interface components { */ EvaluatorInfo: { /** - * Name - * @description Evaluator name - */ - name: string; - /** - * Version - * @description Evaluator version + * Config Schema + * @description JSON Schema for config */ - version: string; + config_schema: { + [key: string]: unknown; + }; /** * Description * @description Evaluator description */ description: string; + /** + * Name + * @description Evaluator name + */ + name: string; /** * Requires Api Key * @description Whether evaluator requires API key @@ -1950,12 +1920,10 @@ export interface components { */ timeout_ms: number; /** - * Config Schema - * @description JSON Schema for config + * Version + * @description Evaluator version */ - config_schema: { - [key: string]: unknown; - }; + version: string; }; /** * EvaluatorResult @@ -1973,16 +1941,21 @@ export interface components { * - Observability systems to monitor evaluator health separately from validation outcomes */ EvaluatorResult: { - /** - * Matched - * @description Whether the pattern matched - */ - matched: boolean; /** * Confidence * @description Confidence in the evaluation */ confidence: number; + /** + * Error + * @description Error message if evaluation failed internally. When set, matched=False is due to error, not actual evaluation. + */ + error?: string | null; + /** + * Matched + * @description Whether the pattern matched + */ + matched: boolean; /** * Message * @description Explanation of the result @@ -1995,11 +1968,6 @@ export interface components { metadata?: { [key: string]: unknown; } | null; - /** - * Error - * @description Error message if evaluation failed internally. When set, matched=False is due to error, not actual evaluation. - */ - error?: string | null; }; /** * EvaluatorSchema @@ -2009,11 +1977,6 @@ export interface components { * This schema is registered via initAgent for validation and UI purposes. */ EvaluatorSchema: { - /** - * Name - * @description Unique evaluator name - */ - name: string; /** * Config Schema * @description JSON Schema for evaluator config validation @@ -2026,20 +1989,60 @@ export interface components { * @description Optional description */ description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; }; /** * EvaluatorSchemaItem * @description Evaluator schema summary for list response. */ EvaluatorSchemaItem: { - /** Name */ - name: string; - /** Description */ - description: string | null; /** Config Schema */ config_schema: { [key: string]: unknown; }; + /** Description */ + description: string | null; + /** Name */ + name: string; + }; + /** + * EvaluatorSpec + * @description Evaluator specification. See GET /evaluators for available evaluators and schemas. + * + * Evaluator reference formats: + * - Built-in: "regex", "list", "json", "sql" + * - External: "galileo.luna2" (requires agent-control-evaluators[galileo]) + * - Agent-scoped: "my-agent:my-evaluator" (validated in endpoint, not here) + */ + EvaluatorSpec: { + /** + * Config + * @description Evaluator-specific configuration + * @example { + * "pattern": "\\d{3}-\\d{2}-\\d{4}" + * } + * @example { + * "logic": "any", + * "values": [ + * "admin" + * ] + * } + */ + config: { + [key: string]: unknown; + }; + /** + * Name + * @description Evaluator name or agent-scoped reference (agent:evaluator) + * @example regex + * @example list + * @example my-agent:pii-detector + */ + name: string; }; /** * EventQueryRequest @@ -2051,80 +2054,60 @@ export interface components { * trace_id: Filter by trace ID (get all events for a request) * span_id: Filter by span ID (get all events for a function call) * control_execution_id: Filter by specific event ID - * agent_uuid: Filter by agent UUID + * agent_name: Filter by agent identifier * control_ids: Filter by control IDs * actions: Filter by actions (allow, deny, warn, log) - * matched: Filter by matched status - * check_stages: Filter by check stages (pre, post) - * applies_to: Filter by call type (llm_call, tool_call) - * start_time: Filter events after this time - * end_time: Filter events before this time - * limit: Maximum number of events to return - * offset: Offset for pagination - * @example { - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * @example { - * "actions": [ - * "deny", - * "warn" - * ], - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", - * "limit": 50, - * "start_time": "2025-01-09T00:00:00Z" - * } - */ - EventQueryRequest: { - /** - * Trace Id - * @description Filter by trace ID (all events for a request) - */ - trace_id?: string | null; - /** - * Span Id - * @description Filter by span ID (all events for a function) - */ - span_id?: string | null; - /** - * Control Execution Id - * @description Filter by specific event ID - */ - control_execution_id?: string | null; - /** - * Agent Uuid - * @description Filter by agent UUID - */ - agent_uuid?: string | null; - /** - * Control Ids - * @description Filter by control IDs - */ - control_ids?: number[] | null; + * matched: Filter by matched status + * check_stages: Filter by check stages (pre, post) + * applies_to: Filter by call type (llm_call, tool_call) + * start_time: Filter events after this time + * end_time: Filter events before this time + * limit: Maximum number of events to return + * offset: Offset for pagination + * @example { + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * @example { + * "actions": [ + * "deny", + * "warn" + * ], + * "agent_name": "my-agent", + * "limit": 50, + * "start_time": "2025-01-09T00:00:00Z" + * } + */ + EventQueryRequest: { /** * Actions * @description Filter by actions */ actions?: ('allow' | 'deny' | 'warn' | 'log')[] | null; /** - * Matched - * @description Filter by matched status + * Agent Name + * @description Filter by agent identifier */ - matched?: boolean | null; + agent_name?: string | null; + /** + * Applies To + * @description Filter by call types + */ + applies_to?: ('llm_call' | 'tool_call')[] | null; /** * Check Stages * @description Filter by check stages */ check_stages?: ('pre' | 'post')[] | null; /** - * Applies To - * @description Filter by call types + * Control Execution Id + * @description Filter by specific event ID */ - applies_to?: ('llm_call' | 'tool_call')[] | null; + control_execution_id?: string | null; /** - * Start Time - * @description Filter events after this time + * Control Ids + * @description Filter by control IDs */ - start_time?: string | null; + control_ids?: number[] | null; /** * End Time * @description Filter events before this time @@ -2136,12 +2119,32 @@ export interface components { * @default 100 */ limit: number; + /** + * Matched + * @description Filter by matched status + */ + matched?: boolean | null; /** * Offset * @description Pagination offset * @default 0 */ offset: number; + /** + * Span Id + * @description Filter by span ID (all events for a function) + */ + span_id?: string | null; + /** + * Start Time + * @description Filter events after this time + */ + start_time?: string | null; + /** + * Trace Id + * @description Filter by trace ID (all events for a request) + */ + trace_id?: string | null; }; /** * EventQueryResponse @@ -2159,11 +2162,6 @@ export interface components { * @description Matching events */ events: components['schemas']['ControlExecutionEvent'][]; - /** - * Total - * @description Total matching events - */ - total: number; /** * Limit * @description Limit used in query @@ -2174,6 +2172,11 @@ export interface components { * @description Offset used in query */ offset: number; + /** + * Total + * @description Total matching events + */ + total: number; }; /** * GetAgentResponse @@ -2182,16 +2185,16 @@ export interface components { GetAgentResponse: { /** @description Agent metadata */ agent: components['schemas']['Agent']; - /** - * Steps - * @description Steps registered with this agent - */ - steps: components['schemas']['StepSchema'][]; /** * Evaluators * @description Custom evaluators registered with this agent */ evaluators?: components['schemas']['EvaluatorSchema'][]; + /** + * Steps + * @description Steps registered with this agent + */ + steps: components['schemas']['StepSchema'][]; }; /** GetControlDataResponse */ GetControlDataResponse: { @@ -2203,6 +2206,8 @@ export interface components { * @description Response containing control details. */ GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components['schemas']['ControlDefinition'] | null; /** * Id * @description Control ID @@ -2213,8 +2218,6 @@ export interface components { * @description Control name */ name: string; - /** @description Control configuration data (None if not yet configured) */ - data?: components['schemas']['ControlDefinition'] | null; }; /** * GetPolicyControlsResponse @@ -2254,13 +2257,86 @@ export interface components { /** Version */ version: string; }; + /** + * InitAgentEvaluatorRemoval + * @description Details for an evaluator removed during overwrite mode. + */ + InitAgentEvaluatorRemoval: { + /** + * Control Ids + * @description IDs of active controls referencing this evaluator + */ + control_ids?: number[]; + /** + * Control Names + * @description Names of active controls referencing this evaluator + */ + control_names?: string[]; + /** + * Name + * @description Evaluator name removed by overwrite + */ + name: string; + /** + * Referenced By Active Controls + * @description Whether this evaluator is still referenced by active controls + * @default false + */ + referenced_by_active_controls: boolean; + }; + /** + * InitAgentOverwriteChanges + * @description Detailed change summary for initAgent overwrite mode. + */ + InitAgentOverwriteChanges: { + /** + * Evaluator Removals + * @description Per-evaluator removal details, including active control references + */ + evaluator_removals?: components['schemas']['InitAgentEvaluatorRemoval'][]; + /** + * Evaluators Added + * @description Evaluator names added by overwrite + */ + evaluators_added?: string[]; + /** + * Evaluators Removed + * @description Evaluator names removed by overwrite + */ + evaluators_removed?: string[]; + /** + * Evaluators Updated + * @description Existing evaluator names updated by overwrite + */ + evaluators_updated?: string[]; + /** + * Metadata Changed + * @description Whether agent metadata changed + * @default false + */ + metadata_changed: boolean; + /** + * Steps Added + * @description Steps added by overwrite + */ + steps_added?: components['schemas']['StepKey'][]; + /** + * Steps Removed + * @description Steps removed by overwrite + */ + steps_removed?: components['schemas']['StepKey'][]; + /** + * Steps Updated + * @description Existing steps updated by overwrite + */ + steps_updated?: components['schemas']['StepKey'][]; + }; /** * InitAgentRequest * @description Request to initialize or update an agent registration. * @example { * "agent": { * "agent_description": "Handles customer inquiries", - * "agent_id": "550e8400-e29b-41d4-a716-446655440000", * "agent_name": "customer-service-bot", * "agent_version": "1.0.0" * }, @@ -2300,10 +2376,10 @@ export interface components { /** @description Agent metadata including ID, name, and version */ agent: components['schemas']['Agent']; /** - * Steps - * @description List of steps available to the agent + * @description Conflict handling mode for init registration updates. 'strict' preserves existing compatibility checks. 'overwrite' applies latest-init-wins replacement for steps and evaluators. + * @default strict */ - steps?: components['schemas']['StepSchema'][]; + conflict_mode: components['schemas']['ConflictMode']; /** * Evaluators * @description Custom evaluator schemas for config validation @@ -2315,22 +2391,35 @@ export interface components { * @default false */ force_replace: boolean; + /** + * Steps + * @description List of steps available to the agent + */ + steps?: components['schemas']['StepSchema'][]; }; /** * InitAgentResponse * @description Response from agent initialization. */ InitAgentResponse: { + /** + * Controls + * @description Active protection controls for the agent (if policy assigned) + */ + controls?: components['schemas']['Control'][]; /** * Created * @description True if agent was newly created, False if updated */ created: boolean; /** - * Controls - * @description Active protection controls for the agent (if policy assigned) + * Overwrite Applied + * @description True if overwrite mode changed registration data on an existing agent + * @default false */ - controls?: components['schemas']['Control'][]; + overwrite_applied: boolean; + /** @description Detailed list of changes applied in overwrite mode */ + overwrite_changes?: components['schemas']['InitAgentOverwriteChanges']; }; JSONObject: { [key: string]: components['schemas']['JSONValue']; @@ -2390,74 +2479,74 @@ export interface components { * @description Pagination metadata for cursor-based pagination. */ PaginationInfo: { + /** + * Has More + * @description Whether there are more pages available + */ + has_more: boolean; /** * Limit * @description Number of items per page */ limit: number; - /** - * Total - * @description Total number of items - */ - total: number; /** * Next Cursor * @description Cursor for fetching the next page (null if no more pages) */ next_cursor?: string | null; /** - * Has More - * @description Whether there are more pages available + * Total + * @description Total number of items */ - has_more: boolean; + total: number; }; /** * PatchAgentRequest * @description Request to modify an agent (remove steps/evaluators). */ PatchAgentRequest: { - /** - * Remove Steps - * @description Step identifiers to remove from the agent - */ - remove_steps?: components['schemas']['StepKey'][]; /** * Remove Evaluators * @description Evaluator names to remove from the agent */ remove_evaluators?: string[]; + /** + * Remove Steps + * @description Step identifiers to remove from the agent + */ + remove_steps?: components['schemas']['StepKey'][]; }; /** * PatchAgentResponse * @description Response from agent modification. */ PatchAgentResponse: { - /** - * Steps Removed - * @description Step identifiers that were removed - */ - steps_removed?: components['schemas']['StepKey'][]; /** * Evaluators Removed * @description Evaluator names that were removed */ evaluators_removed?: string[]; + /** + * Steps Removed + * @description Step identifiers that were removed + */ + steps_removed?: components['schemas']['StepKey'][]; }; /** * PatchControlRequest * @description Request to update control metadata (name, enabled status). */ PatchControlRequest: { - /** - * Name - * @description New name for the control - */ - name?: string | null; /** * Enabled * @description Enable or disable the control */ enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; }; /** * PatchControlResponse @@ -2465,20 +2554,20 @@ export interface components { */ PatchControlResponse: { /** - * Success - * @description Whether the update succeeded + * Enabled + * @description Current enabled status (if control has data configured) */ - success: boolean; + enabled?: boolean | null; /** * Name * @description Current control name (may have changed) */ name: string; /** - * Enabled - * @description Current enabled status (if control has data configured) + * Success + * @description Whether the update succeeded */ - enabled?: boolean | null; + success: boolean; }; /** * SetControlDataRequest @@ -2498,16 +2587,16 @@ export interface components { }; /** SetPolicyResponse */ SetPolicyResponse: { - /** - * Success - * @description Whether the policy was successfully assigned - */ - success: boolean; /** * Old Policy Id * @description Previous policy id if one was replaced */ old_policy_id?: number | null; + /** + * Success + * @description Whether the policy was successfully assigned + */ + success: boolean; }; /** * StatsResponse @@ -2516,18 +2605,22 @@ export interface components { * Contains agent-level totals (with optional timeseries) and per-control breakdown. * * Attributes: - * agent_uuid: Agent UUID + * agent_name: Agent identifier * time_range: Time range used * totals: Agent-level aggregate statistics (includes timeseries) * controls: Per-control breakdown for discovery and detail */ StatsResponse: { /** - * Agent Uuid - * Format: uuid - * @description Agent UUID + * Agent Name + * @description Agent identifier + */ + agent_name: string; + /** + * Controls + * @description Per-control breakdown */ - agent_uuid: string; + controls: components['schemas']['ControlStats'][]; /** * Time Range * @description Time range used @@ -2535,11 +2628,6 @@ export interface components { time_range: string; /** @description Agent-level aggregate statistics */ totals: components['schemas']['StatsTotals']; - /** - * Controls - * @description Per-control breakdown - */ - controls: components['schemas']['ControlStats'][]; }; /** * StatsTotals @@ -2559,6 +2647,19 @@ export interface components { * timeseries: Time-series data points (only when include_timeseries=true) */ StatsTotals: { + /** + * Action Counts + * @description Action breakdown for matches: {allow, deny, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Error Count + * @description Total errors + * @default 0 + */ + error_count: number; /** * Execution Count * @description Total executions @@ -2576,19 +2677,6 @@ export interface components { * @default 0 */ non_match_count: number; - /** - * Error Count - * @description Total errors - * @default 0 - */ - error_count: number; - /** - * Action Counts - * @description Action breakdown for matches: {allow, deny, warn, log} - */ - action_counts?: { - [key: string]: number; - }; /** * Timeseries * @description Time-series data points (only when include_timeseries=true) @@ -2597,41 +2685,41 @@ export interface components { }; /** * Step - * @description Runtime payload for an agent step invocation. - */ - Step: { - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; + * @description Runtime payload for an agent step invocation. + */ + Step: { + /** @description Optional context (conversation history, metadata, etc.) */ + context?: components['schemas']['JSONObject'] | null; + /** @description Input content for this step */ + input: components['schemas']['JSONValue']; /** * Name * @description Step name (tool name or model/chain id) */ name: string; - /** @description Input content for this step */ - input: components['schemas']['JSONValue']; /** @description Output content for this step (None for pre-checks) */ output?: components['schemas']['JSONValue'] | null; - /** @description Optional context (conversation history, metadata, etc.) */ - context?: components['schemas']['JSONObject'] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; }; /** * StepKey * @description Identifies a registered step schema by type and name. */ StepKey: { - /** - * Type - * @description Step type - */ - type: string; /** * Name * @description Registered step name */ name: string; + /** + * Type + * @description Step type + */ + type: string; }; /** * StepSchema @@ -2675,16 +2763,6 @@ export interface components { * } */ StepSchema: { - /** - * Type - * @description Step type for this schema (e.g., 'tool', 'llm') - */ - type: string; - /** - * Name - * @description Unique name for the step - */ - name: string; /** * Description * @description Optional description of the step @@ -2697,6 +2775,18 @@ export interface components { input_schema?: { [key: string]: unknown; } | null; + /** + * Metadata + * @description Additional metadata for the step + */ + metadata?: { + [key: string]: unknown; + } | null; + /** + * Name + * @description Unique name for the step + */ + name: string; /** * Output Schema * @description JSON schema describing step output @@ -2705,12 +2795,10 @@ export interface components { [key: string]: unknown; } | null; /** - * Metadata - * @description Additional metadata for the step + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') */ - metadata?: { - [key: string]: unknown; - } | null; + type: string; }; /** * TimeseriesBucket @@ -2730,11 +2818,27 @@ export interface components { */ TimeseriesBucket: { /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) + * Action Counts + * @description Action breakdown: {allow, deny, warn, log} */ - timestamp: string; + action_counts?: { + [key: string]: number; + }; + /** + * Avg Confidence + * @description Average confidence score + */ + avg_confidence?: number | null; + /** + * Avg Duration Ms + * @description Average duration (ms) + */ + avg_duration_ms?: number | null; + /** + * Error Count + * @description Errors in bucket + */ + error_count: number; /** * Execution Count * @description Total executions in bucket @@ -2751,27 +2855,11 @@ export interface components { */ non_match_count: number; /** - * Error Count - * @description Errors in bucket - */ - error_count: number; - /** - * Action Counts - * @description Action breakdown: {allow, deny, warn, log} - */ - action_counts?: { - [key: string]: number; - }; - /** - * Avg Confidence - * @description Average confidence score - */ - avg_confidence?: number | null; - /** - * Avg Duration Ms - * @description Average duration (ms) + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) */ - avg_duration_ms?: number | null; + timestamp: string; }; /** * UpdateEvaluatorConfigRequest @@ -2779,10 +2867,12 @@ export interface components { */ UpdateEvaluatorConfigRequest: { /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + * Config + * @description Evaluator-specific configuration */ - name: string; + config: { + [key: string]: unknown; + }; /** * Description * @description Optional description @@ -2794,15 +2884,33 @@ export interface components { */ evaluator: string; /** - * Config - * @description Evaluator-specific configuration + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) */ - config: { - [key: string]: unknown; - }; + name: string; + }; + /** + * ValidateControlDataRequest + * @description Request to validate control configuration data without saving. + */ + ValidateControlDataRequest: { + /** @description Control configuration data to validate */ + data: components['schemas']['ControlDefinition']; + }; + /** ValidateControlDataResponse */ + ValidateControlDataResponse: { + /** + * Success + * @description Whether the control data is valid + */ + success: boolean; }; /** ValidationError */ ValidationError: { + /** Context */ + ctx?: Record; + /** Input */ + input?: unknown; /** Location */ loc: (string | number)[]; /** Message */ @@ -2885,12 +2993,12 @@ export interface operations { }; }; }; - get_agent_api_v1_agents__agent_id__get: { + get_agent_api_v1_agents__agent_name__get: { parameters: { query?: never; header?: never; path: { - agent_id: string; + agent_name: string; }; cookie?: never; }; @@ -2916,12 +3024,12 @@ export interface operations { }; }; }; - patch_agent_api_v1_agents__agent_id__patch: { + patch_agent_api_v1_agents__agent_name__patch: { parameters: { query?: never; header?: never; path: { - agent_id: string; + agent_name: string; }; cookie?: never; }; @@ -2951,106 +3059,12 @@ export interface operations { }; }; }; - set_agent_policy_api_v1_agents__agent_id__policy__policy_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - policy_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success status with previous policy ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SetPolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_policy_api_v1_agents__agent_id__policy_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Policy ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetPolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_agent_policy_api_v1_agents__agent_id__policy_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeletePolicyResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_controls_api_v1_agents__agent_id__controls_get: { + list_agent_controls_api_v1_agents__agent_name__controls_get: { parameters: { query?: never; header?: never; path: { - agent_id: string; + agent_name: string; }; cookie?: never; }; @@ -3076,7 +3090,7 @@ export interface operations { }; }; }; - list_agent_evaluators_api_v1_agents__agent_id__evaluators_get: { + list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { parameters: { query?: { cursor?: string | null; @@ -3084,7 +3098,7 @@ export interface operations { }; header?: never; path: { - agent_id: string; + agent_name: string; }; cookie?: never; }; @@ -3110,58 +3124,25 @@ export interface operations { }; }; }; - get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - evaluator_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schema details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorSchemaItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_policy_api_v1_policies_put: { + get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreatePolicyRequest']; + query?: never; + header?: never; + path: { + agent_name: string; + evaluator_name: string; }; + cookie?: never; }; + requestBody?: never; responses: { - /** @description Created policy ID */ + /** @description Evaluator schema details */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['CreatePolicyResponse']; + 'application/json': components['schemas']['EvaluatorSchemaItem']; }; }; /** @description Validation Error */ @@ -3175,25 +3156,24 @@ export interface operations { }; }; }; - add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post: { + get_agent_policy_api_v1_agents__agent_name__policy_get: { parameters: { query?: never; header?: never; path: { - policy_id: number; - control_id: number; + agent_name: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Success confirmation */ + /** @description Policy ID */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AssocResponse']; + 'application/json': components['schemas']['GetPolicyResponse']; }; }; /** @description Validation Error */ @@ -3207,13 +3187,12 @@ export interface operations { }; }; }; - remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete: { + delete_agent_policy_api_v1_agents__agent_name__policy_delete: { parameters: { query?: never; header?: never; path: { - policy_id: number; - control_id: number; + agent_name: string; }; cookie?: never; }; @@ -3225,7 +3204,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AssocResponse']; + 'application/json': components['schemas']['DeletePolicyResponse']; }; }; /** @description Validation Error */ @@ -3239,24 +3218,25 @@ export interface operations { }; }; }; - list_policy_controls_api_v1_policies__policy_id__controls_get: { + set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post: { parameters: { query?: never; header?: never; path: { + agent_name: string; policy_id: number; }; cookie?: never; }; requestBody?: never; responses: { - /** @description List of control IDs */ + /** @description Success status with previous policy ID */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['GetPolicyControlsResponse']; + 'application/json': components['schemas']['SetPolicyResponse']; }; }; /** @description Validation Error */ @@ -3348,6 +3328,39 @@ export interface operations { }; }; }; + validate_control_data_api_v1_controls_validate_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['ValidateControlDataRequest']; + }; + }; + responses: { + /** @description Validation result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ValidateControlDataResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; get_control_api_v1_controls__control_id__get: { parameters: { query?: never; @@ -3514,6 +3527,42 @@ export interface operations { }; }; }; + evaluate_api_v1_evaluation_post: { + parameters: { + query?: never; + header?: { + 'X-Trace-Id'?: string | null; + 'X-Span-Id'?: string | null; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EvaluationRequest']; + }; + }; + responses: { + /** @description Safety analysis result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluationResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; list_evaluator_configs_api_v1_evaluator_configs_get: { parameters: { query?: { @@ -3681,42 +3730,6 @@ export interface operations { }; }; }; - evaluate_api_v1_evaluation_post: { - parameters: { - query?: never; - header?: { - 'X-Trace-Id'?: string | null; - 'X-Span-Id'?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EvaluationRequest']; - }; - }; - responses: { - /** @description Safety analysis result */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluationResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; get_evaluators_api_v1_evaluators_get: { parameters: { query?: never; @@ -3808,7 +3821,7 @@ export interface operations { get_stats_api_v1_observability_stats_get: { parameters: { query: { - agent_uuid: string; + agent_name: string; time_range?: | '1m' | '5m' @@ -3850,7 +3863,7 @@ export interface operations { get_control_stats_api_v1_observability_stats_controls__control_id__get: { parameters: { query: { - agent_uuid: string; + agent_name: string; time_range?: | '1m' | '5m' @@ -3913,6 +3926,134 @@ export interface operations { }; }; }; + create_policy_api_v1_policies_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreatePolicyRequest']; + }; + }; + responses: { + /** @description Created policy ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['CreatePolicyResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_policy_controls_api_v1_policies__policy_id__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of control IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetPolicyControlsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + add_control_to_policy_api_v1_policies__policy_id__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AssocResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + remove_control_from_policy_api_v1_policies__policy_id__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + policy_id: number; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AssocResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; health_check_health_get: { parameters: { query?: never; diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index d60c0705..ca2f944e 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -106,11 +106,7 @@ export type ListControlsResponse = components['schemas']['ListControlsResponse']; // AgentRef - reference to an agent (for used_by_agents) -// Note: This will be in generated types after running pnpm fetch-api-types -export type AgentRef = { - agent_id: string; - agent_name: string; -}; +export type AgentRef = components['schemas']['AgentRef']; // Helper type to extract query parameters from operations type ExtractQueryParams = T extends { parameters: { query?: infer Q } } @@ -134,10 +130,10 @@ export type ListAgentsQueryParams = ExtractQueryParams< operations['list_agents_api_v1_agents_get'] >; export type GetAgentPathParams = ExtractPathParams< - operations['get_agent_api_v1_agents__agent_id__get'] + operations['get_agent_api_v1_agents__agent_name__get'] >; export type GetAgentControlsPathParams = ExtractPathParams< - operations['list_agent_controls_api_v1_agents__agent_id__controls_get'] + operations['list_agent_controls_api_v1_agents__agent_name__controls_get'] >; // Request body types diff --git a/ui/src/core/hooks/query-hooks/use-agent-controls.ts b/ui/src/core/hooks/query-hooks/use-agent-controls.ts index baaedaa9..0cb18df3 100644 --- a/ui/src/core/hooks/query-hooks/use-agent-controls.ts +++ b/ui/src/core/hooks/query-hooks/use-agent-controls.ts @@ -9,15 +9,15 @@ import type { /** * Query hook to fetch active controls for an agent * - * @param agentId - UUID of the agent (required) + * @param agentName - Immutable agent name (required) */ export function useAgentControls( - agentId: GetAgentControlsPathParams['agent_id'] + agentName: GetAgentControlsPathParams['agent_name'] ) { return useQuery({ - queryKey: ['agent', agentId, 'controls'], + queryKey: ['agent', agentName, 'controls'], queryFn: async () => { - const { data, error } = await api.agents.getControls(agentId); + const { data, error } = await api.agents.getControls(agentName); if (error) throw error; return data; }, diff --git a/ui/src/core/hooks/query-hooks/use-agent-monitor.ts b/ui/src/core/hooks/query-hooks/use-agent-monitor.ts index cb1198a8..5e818c08 100644 --- a/ui/src/core/hooks/query-hooks/use-agent-monitor.ts +++ b/ui/src/core/hooks/query-hooks/use-agent-monitor.ts @@ -19,7 +19,7 @@ export type TimeseriesBucket = components['schemas']['TimeseriesBucket']; export type StatsTotals = components['schemas']['StatsTotals']; export function useAgentMonitor( - agentUuid: string, + agentName: string, timeRange: TimeRange = '1h', options?: { enabled?: boolean; @@ -30,13 +30,13 @@ export function useAgentMonitor( return useQuery({ queryKey: [ 'agent-monitor', - agentUuid, + agentName, timeRange, options?.includeTimeseries ?? false, ], queryFn: async (): Promise => { const { data, error } = await api.observability.getStats({ - agent_uuid: agentUuid, + agent_name: agentName, time_range: timeRange, include_timeseries: options?.includeTimeseries ?? false, }); @@ -47,7 +47,7 @@ export function useAgentMonitor( return data; }, - enabled: options?.enabled !== false && !!agentUuid, + enabled: options?.enabled !== false && !!agentName, refetchInterval: options?.refetchInterval ?? 5000, // Default 5 seconds refetchIntervalInBackground: false, // Pause polling when tab is not visible placeholderData: keepPreviousData, // Keep showing previous data while loading new time range diff --git a/ui/src/core/hooks/query-hooks/use-agent.ts b/ui/src/core/hooks/query-hooks/use-agent.ts index 6c728ca4..e01f8a30 100644 --- a/ui/src/core/hooks/query-hooks/use-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-agent.ts @@ -5,13 +5,13 @@ import { api } from '@/core/api/client'; import type { GetAgentPathParams, GetAgentResponse } from '@/core/api/types'; /** - * Query hook to fetch a single agent by ID + * Query hook to fetch a single agent by identifier. * - * @param agentId - UUID of the agent (required) + * @param agentName - Immutable agent name (required) * */ export function useAgent( - agentId: GetAgentPathParams['agent_id'], + agentName: GetAgentPathParams['agent_name'], options?: Omit< UseQueryOptions< GetAgentResponse, @@ -23,12 +23,12 @@ export function useAgent( > ): UseQueryResult { const { enabled, ...rest } = options ?? {}; - const isEnabled = enabled ?? Boolean(agentId); + const isEnabled = enabled ?? Boolean(agentName); return useQuery({ - queryKey: ['agent', agentId], + queryKey: ['agent', agentName], queryFn: async () => { - const { data, error } = await api.agents.get(agentId); + const { data, error } = await api.agents.get(agentName); if (error) throw error; return data; }, diff --git a/ui/src/core/hooks/query-hooks/use-has-monitor-data.ts b/ui/src/core/hooks/query-hooks/use-has-monitor-data.ts index e66fdc92..e6276b78 100644 --- a/ui/src/core/hooks/query-hooks/use-has-monitor-data.ts +++ b/ui/src/core/hooks/query-hooks/use-has-monitor-data.ts @@ -34,18 +34,18 @@ function getStoredTimeRange(): TimeRange { * Uses the stored time range preference from localStorage. */ export function useHasMonitorData( - agentUuid: string, + agentName: string, options?: { enabled?: boolean; } ) { return useQuery({ - queryKey: ['has-monitor-data', agentUuid], + queryKey: ['has-monitor-data', agentName], queryFn: async () => { const timeRange = getStoredTimeRange(); const { data, error } = await api.observability.getStats({ - agent_uuid: agentUuid, + agent_name: agentName, time_range: timeRange, include_timeseries: false, // Don't need timeseries, just totals }); @@ -61,7 +61,7 @@ export function useHasMonitorData( return hasData; }, - enabled: options?.enabled !== false && !!agentUuid, + enabled: options?.enabled !== false && !!agentName, staleTime: 30000, // Consider data fresh for 30 seconds refetchOnWindowFocus: false, // Don't refetch on window focus }); diff --git a/ui/src/core/layouts/app-layout.tsx b/ui/src/core/layouts/app-layout.tsx index 40b0c142..077ceb51 100644 --- a/ui/src/core/layouts/app-layout.tsx +++ b/ui/src/core/layouts/app-layout.tsx @@ -236,10 +236,10 @@ export function AppLayout({ children }: AppLayoutProps) { <> {allAgents.map((agent) => ( ))} diff --git a/ui/src/core/page-components/agent-detail/agent-detail.tsx b/ui/src/core/page-components/agent-detail/agent-detail.tsx index 2405b4d3..aa251f59 100644 --- a/ui/src/core/page-components/agent-detail/agent-detail.tsx +++ b/ui/src/core/page-components/agent-detail/agent-detail.tsx @@ -328,9 +328,9 @@ const AgentDetailPage = ({ agentId, defaultTab }: AgentDetailPageProps) => { - {agent?.agent.agent_id && activeTab === 'monitor' ? ( + {agent?.agent.agent_name && activeTab === 'monitor' ? ( ) : null} diff --git a/ui/src/core/page-components/agent-detail/modals/control-store/index.tsx b/ui/src/core/page-components/agent-detail/modals/control-store/index.tsx index e5d7609d..de09c575 100644 --- a/ui/src/core/page-components/agent-detail/modals/control-store/index.tsx +++ b/ui/src/core/page-components/agent-detail/modals/control-store/index.tsx @@ -37,7 +37,7 @@ import { AddNewControlModal } from '../add-new-control'; import { EditControlContent } from '../edit-control/edit-control-content'; import { sanitizeControlNamePart } from '../edit-control/utils'; -// Extended ControlSummary with used_by_agent (until API types are regenerated) +// Extended ControlSummary with optional used_by_agent type ControlSummaryWithAgent = ControlSummary & { used_by_agent?: AgentRef | null; }; @@ -289,7 +289,7 @@ export function ControlStoreModal({ ); } // Link to agent controls tab with control name filter - const href = `/agents/${agent.agent_id}/controls?q=${encodeURIComponent(control.name)}`; + const href = `/agents/${agent.agent_name}/controls?q=${encodeURIComponent(control.name)}`; return ( { }, [data]); const handleRowClick = (agent: AgentTableRow) => { - router.push(`/agents/${agent.agent_id}`); + router.push(`/agents/${agent.agent_name}`); }; // Define table columns diff --git a/ui/tests/agent-detail.spec.ts b/ui/tests/agent-detail.spec.ts index ffdc39cd..938dec8a 100644 --- a/ui/tests/agent-detail.spec.ts +++ b/ui/tests/agent-detail.spec.ts @@ -3,7 +3,7 @@ import type { AgentControlsResponse, GetAgentResponse } from '@/core/api/types'; import { expect, mockData, mockRoutes, test } from './fixtures'; test.describe('Agent Detail Page', () => { - const agentId = 'agent-1'; + const agentId = 'customer-support-bot'; const agentUrl = `/agents/${agentId}/controls`; // Type-safe access to mock agent data @@ -638,7 +638,7 @@ test.describe('Agent Detail - Empty State', () => { }); }); - await page.goto('/agents/agent-1/controls'); + await page.goto('/agents/customer-support-bot/controls'); // Check for empty state message await expect(page.getByText('No controls configured')).toBeVisible(); diff --git a/ui/tests/agent-stats.spec.ts b/ui/tests/agent-stats.spec.ts index 4d293fba..5ebac0e7 100644 --- a/ui/tests/agent-stats.spec.ts +++ b/ui/tests/agent-stats.spec.ts @@ -3,10 +3,10 @@ import { expect, mockData, mockRoutes, test } from './fixtures'; test.describe('Agent Monitor Tab', () => { test.beforeEach(async ({ mockedPage }) => { // Navigate to agent detail page - await mockedPage.goto('/agents/agent-1/monitor'); + await mockedPage.goto('/agents/customer-support-bot/monitor'); // Wait for the page to load await expect( - mockedPage.getByRole('heading', { name: 'Customer Support Bot' }) + mockedPage.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); }); @@ -148,9 +148,9 @@ test.describe('Agent Monitor Tab - Empty State', () => { await mockRoutes.stats(page, { data: mockData.emptyStats }); // Navigate to agent detail page - await page.goto('/agents/agent-1/monitor'); + await page.goto('/agents/customer-support-bot/monitor'); await expect( - page.getByRole('heading', { name: 'Customer Support Bot' }) + page.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); // Navigate to stats tab @@ -235,9 +235,9 @@ test.describe('Agent Monitor Tab - Refetch Flow', () => { }); // Navigate to agent detail page - await page.goto('/agents/agent-1/monitor'); + await page.goto('/agents/customer-support-bot/monitor'); await expect( - page.getByRole('heading', { name: 'Customer Support Bot' }) + page.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); // Navigate to stats tab @@ -272,9 +272,9 @@ test.describe('Agent Monitor Tab - Error State', () => { }); // Navigate to agent detail page - await page.goto('/agents/agent-1/monitor'); + await page.goto('/agents/customer-support-bot/monitor'); await expect( - page.getByRole('heading', { name: 'Customer Support Bot' }) + page.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); // Navigate to stats tab diff --git a/ui/tests/control-store.spec.ts b/ui/tests/control-store.spec.ts index 03f9c2bf..ed984ed4 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'; import { expect, mockData, test } from './fixtures'; -const agentUrl = '/agents/agent-1/controls'; +const agentUrl = '/agents/customer-support-bot/controls'; async function openControlStoreModal(page: Page) { await page.goto(agentUrl); @@ -61,15 +61,15 @@ test.describe('Control Store Modal', () => { test('displays agent links in Agent column', async ({ mockedPage }) => { const modal = await openControlStoreModal(mockedPage); - // PII Detection is used by Customer Support Bot + // PII Detection is used by customer-support-bot const agentLink = modal - .getByRole('link', { name: 'Customer Support Bot' }) + .getByRole('link', { name: 'customer-support-bot' }) .first(); await expect(agentLink).toBeVisible(); // Link includes query param to filter by control name await expect(agentLink).toHaveAttribute( 'href', - '/agents/agent-1/controls?q=PII%20Detection' + '/agents/customer-support-bot/controls?q=PII%20Detection' ); }); @@ -720,7 +720,7 @@ test.describe('Control Store - Loading States', () => { }); }); - await page.goto('/agents/agent-1/controls'); + await page.goto('/agents/customer-support-bot/controls'); // Open the control store modal await page.getByTestId('add-control-button').first().click(); diff --git a/ui/tests/evaluators/helpers.ts b/ui/tests/evaluators/helpers.ts index 40e38037..0ddd940a 100644 --- a/ui/tests/evaluators/helpers.ts +++ b/ui/tests/evaluators/helpers.ts @@ -4,7 +4,7 @@ import { expect, type Page } from '@playwright/test'; -const AGENT_URL = '/agents/agent-1/controls'; +const AGENT_URL = '/agents/customer-support-bot/controls'; /** * Opens the control store and selects an evaluator to create a new control diff --git a/ui/tests/fixtures.ts b/ui/tests/fixtures.ts index de77e844..69725b20 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -20,8 +20,7 @@ import type { StatsResponse } from '@/core/hooks/query-hooks/use-agent-monitor'; // Satisfies ensures type checking while allowing inference of literal types const agentsList: AgentSummary[] = [ { - agent_id: 'agent-1', - agent_name: 'Customer Support Bot', + agent_name: 'customer-support-bot', policy_id: 1, created_at: '2024-01-01T00:00:00Z', step_count: 5, @@ -29,8 +28,7 @@ const agentsList: AgentSummary[] = [ active_controls_count: 3, }, { - agent_id: 'agent-2', - agent_name: 'Data Analysis Agent', + agent_name: 'data-analysis-agent', policy_id: 2, created_at: '2024-01-02T00:00:00Z', step_count: 3, @@ -38,8 +36,7 @@ const agentsList: AgentSummary[] = [ active_controls_count: 2, }, { - agent_id: 'agent-3', - agent_name: 'Code Review Assistant', + agent_name: 'code-review-assistant', policy_id: 3, created_at: '2024-01-03T00:00:00Z', step_count: 8, @@ -60,8 +57,7 @@ const agentsResponse: ListAgentsResponse = { const agentResponse: GetAgentResponse = { agent: { - agent_id: 'agent-1', - agent_name: 'Customer Support Bot', + agent_name: 'customer-support-bot', agent_description: 'Handles customer inquiries and support tickets', agent_created_at: '2024-01-01T00:00:00Z', agent_updated_at: '2024-01-15T00:00:00Z', @@ -148,7 +144,7 @@ const controlsResponse: AgentControlsResponse = { // Control summaries for GET /api/v1/controls (list all controls) const controlSummariesList: (ControlSummary & { - used_by_agent?: { agent_id: string; agent_name: string } | null; + used_by_agent?: { agent_name: string } | null; })[] = [ { id: 1, @@ -159,7 +155,7 @@ const controlSummariesList: (ControlSummary & { step_types: ['llm'], stages: ['post'], tags: ['pii', 'compliance'], - used_by_agent: { agent_id: 'agent-1', agent_name: 'Customer Support Bot' }, + used_by_agent: { agent_name: 'customer-support-bot' }, }, { id: 2, @@ -170,7 +166,7 @@ const controlSummariesList: (ControlSummary & { step_types: ['tool'], stages: ['pre'], tags: ['security'], - used_by_agent: { agent_id: 'agent-2', agent_name: 'Data Analysis Agent' }, + used_by_agent: { agent_name: 'data-analysis-agent' }, }, { id: 3, @@ -269,7 +265,7 @@ const evaluatorsResponse: EvaluatorsResponse = { }; const statsResponse: StatsResponse = { - agent_uuid: 'agent-1', + agent_name: 'customer-support-bot', time_range: '1h', totals: { execution_count: 430, @@ -330,7 +326,7 @@ const statsResponse: StatsResponse = { }; const emptyStatsResponse: StatsResponse = { - agent_uuid: 'agent-1', + agent_name: 'customer-support-bot', time_range: '1h', totals: { execution_count: 0, diff --git a/ui/tests/home.spec.ts b/ui/tests/home.spec.ts index d2a95886..bc8751d2 100644 --- a/ui/tests/home.spec.ts +++ b/ui/tests/home.spec.ts @@ -46,12 +46,12 @@ test.describe('Home Page - Agents Overview', () => { ); // Only the matching agent should be visible - await expect(mockedPage.getByText('Customer Support Bot')).toBeVisible(); + await expect(mockedPage.getByText('customer-support-bot')).toBeVisible(); // Non-matching agents should be hidden - await expect(mockedPage.getByText('Data Analysis Agent')).not.toBeVisible(); + await expect(mockedPage.getByText('data-analysis-agent')).not.toBeVisible(); await expect( - mockedPage.getByText('Code Review Assistant') + mockedPage.getByText('code-review-assistant') ).not.toBeVisible(); // Clear search to show all agents again @@ -62,7 +62,7 @@ test.describe('Home Page - Agents Overview', () => { // Wait for a previously hidden agent to become visible (confirms filter was cleared) // This is more reliable than waiting for an API response that might not happen - await expect(mockedPage.getByText('Data Analysis Agent')).toBeVisible({ + await expect(mockedPage.getByText('data-analysis-agent')).toBeVisible({ timeout: 5000, }); @@ -129,7 +129,7 @@ test.describe('Home Page - Agents Overview', () => { // Verify navigation to agent detail page // Since stats mock returns data, it will redirect to monitor tab await expect(mockedPage).toHaveURL( - `/agents/${firstAgent.agent_id}/monitor` + `/agents/${firstAgent.agent_name}/monitor` ); }); diff --git a/ui/tests/search-input.spec.ts b/ui/tests/search-input.spec.ts index 61a6bdeb..4ec1f8d2 100644 --- a/ui/tests/search-input.spec.ts +++ b/ui/tests/search-input.spec.ts @@ -38,7 +38,7 @@ test.describe('SearchInput - Query Param Syncing', () => { await expect(searchInput).toHaveValue('Customer'); // Verify filtered results are shown - await expect(mockedPage.getByText('Customer Support Bot')).toBeVisible(); + await expect(mockedPage.getByText('customer-support-bot')).toBeVisible(); }); test('clear button removes query param from URL', async ({ mockedPage }) => { @@ -87,8 +87,8 @@ test.describe('SearchInput - Query Param Syncing', () => { ); // Navigate away - await mockedPage.getByText('Customer Support Bot').click(); - await expect(mockedPage).toHaveURL(/\/agents\/agent-1/); + await mockedPage.getByText('customer-support-bot').click(); + await expect(mockedPage).toHaveURL(/\/agents\/customer-support-bot/); // Go back await mockedPage.goBack(); @@ -105,13 +105,13 @@ test.describe('SearchInput - Query Param Syncing', () => { await expect(searchInputAfterBack).toHaveValue('Customer'); // Verify filtered results are still shown - await expect(mockedPage.getByText('Customer Support Bot')).toBeVisible(); + await expect(mockedPage.getByText('customer-support-bot')).toBeVisible(); }); }); test.describe('SearchInput - Agent Detail Page', () => { test('syncs search value to URL query param (q)', async ({ mockedPage }) => { - await mockedPage.goto('/agents/agent-1/controls'); + await mockedPage.goto('/agents/customer-support-bot/controls'); const searchInput = mockedPage.getByPlaceholder('Search controls...'); await searchInput.fill('PII'); @@ -124,7 +124,7 @@ test.describe('SearchInput - Agent Detail Page', () => { }); test('reads search value from URL on page load', async ({ mockedPage }) => { - await mockedPage.goto('/agents/agent-1/controls?q=PII'); + await mockedPage.goto('/agents/customer-support-bot/controls?q=PII'); // Wait for page to load await expect(mockedPage.getByRole('table')).toBeVisible();