From 4c363ad53e39c85971d872204d4b7ec966c425e3 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 13:47:08 -0500 Subject: [PATCH 01/32] feat: cut over to optional multi-policy and direct agent controls --- models/src/agent_control_models/server.py | 31 +- sdks/python/src/agent_control/__init__.py | 106 +- sdks/python/src/agent_control/agents.py | 120 +- .../src/agent_control/control_decorators.py | 2 +- sdks/python/src/agent_control/controls.py | 11 +- sdks/python/src/agent_control/policies.py | 6 +- .../overlays/method-names.overlay.yaml | 27 +- .../src/generated/funcs/agents-add-control.ts | 191 + ...-update-policy.ts => agents-add-policy.ts} | 40 +- ...s-get-policy.ts => agents-get-policies.ts} | 34 +- .../generated/funcs/agents-list-controls.ts | 5 +- .../src/generated/funcs/agents-list.ts | 2 +- ...ts => agents-remove-all-agent-policies.ts} | 40 +- .../generated/funcs/agents-remove-control.ts | 191 + .../generated/funcs/agents-remove-policy.ts | 191 + .../src/generated/funcs/controls-delete.ts | 4 +- .../models/agent-controls-response.ts | 2 +- .../src/generated/models/agent-summary.ts | 10 +- .../models/delete-control-response.ts | 12 +- .../models/delete-policy-response.ts | 34 - ...onse.ts => get-agent-policies-response.ts} | 22 +- sdks/typescript/src/generated/models/index.ts | 4 +- .../generated/models/init-agent-response.ts | 2 +- ...gents-agent-id-controls-control-id-post.ts | 46 + ...agents-agent-id-policies-policy-id-post.ts | 46 + ...cy-api-v1-agents-agent-id-policy-delete.ts | 42 - ...ntrol-api-v1-controls-control-id-delete.ts | 2 +- ...ies-api-v1-agents-agent-id-policies-get.ts | 42 + ...olicy-api-v1-agents-agent-id-policy-get.ts | 42 - .../src/generated/models/operations/index.ts | 9 +- ...nts-agent-id-controls-control-id-delete.ts | 49 + ...ents-agent-id-policies-policy-id-delete.ts | 46 + ...-api-v1-agents-agent-id-policies-delete.ts | 42 + ...1-agents-agent-id-policy-policy-id-post.ts | 46 - .../generated/models/set-policy-response.ts | 47 - sdks/typescript/src/generated/sdk/agents.ts | 142 +- sdks/typescript/src/generated/sdk/controls.ts | 4 +- ...6e02_agent_policy_m2m_and_direct_agent_.py | 80 + .../agent_control_server/endpoints/agents.py | 500 +- .../endpoints/controls.py | 113 +- server/src/agent_control_server/models.py | 34 +- .../agent_control_server/services/controls.py | 33 +- server/tests/conftest.py | 2 + server/tests/test_agents_additional.py | 85 +- server/tests/test_controls_additional.py | 3 +- server/tests/test_error_handling.py | 14 +- server/tests/test_evaluation_e2e.py | 8 +- server/tests/test_init_agent.py | 58 +- server/tests/test_new_features.py | 10 +- server/tests/test_policy_integration.py | 39 +- server/tests/test_services_controls.py | 39 +- server/tests/utils.py | 2 +- ui/src/core/api/client.ts | 24 +- ui/src/core/api/generated/api-types.ts | 7978 +++++++++-------- .../query-hooks/use-add-control-to-agent.ts | 67 +- .../hooks/query-hooks/use-delete-control.ts | 4 +- ui/tests/control-store.spec.ts | 70 +- ui/tests/fixtures.ts | 6 +- 58 files changed, 5997 insertions(+), 4864 deletions(-) create mode 100644 sdks/typescript/src/generated/funcs/agents-add-control.ts rename sdks/typescript/src/generated/funcs/{agents-update-policy.ts => agents-add-policy.ts} (81%) rename sdks/typescript/src/generated/funcs/{agents-get-policy.ts => agents-get-policies.ts} (84%) rename sdks/typescript/src/generated/funcs/{agents-delete-policy.ts => agents-remove-all-agent-policies.ts} (82%) create mode 100644 sdks/typescript/src/generated/funcs/agents-remove-control.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-remove-policy.ts delete mode 100644 sdks/typescript/src/generated/models/delete-policy-response.ts rename sdks/typescript/src/generated/models/{get-policy-response.ts => get-agent-policies-response.ts} (51%) create mode 100644 sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts create mode 100644 sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts delete mode 100644 sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-id-policy-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-id-policy-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts delete mode 100644 sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-id-policy-policy-id-post.ts delete mode 100644 sdks/typescript/src/generated/models/set-policy-response.ts create mode 100644 server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index 4e2238eb..6be72aa9 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -102,7 +102,7 @@ class InitAgentResponse(BaseModel): ) controls: list[Control] = Field( default_factory=list, - description="Active protection controls for the agent (if policy assigned)", + description="Active protection controls for the agent", ) @@ -119,24 +119,15 @@ class CreatePolicyResponse(BaseModel): policy_id: int = Field(description="Identifier of the created policy") -class SetPolicyResponse(BaseModel): - success: bool = Field(description="Whether the policy was successfully assigned") - old_policy_id: int | None = Field( - default=None, description="Previous policy id if one was replaced" +class GetAgentPoliciesResponse(BaseModel): + policy_ids: list[int] = Field( + default_factory=list, description="IDs of policies associated with the agent" ) -class GetPolicyResponse(BaseModel): - policy_id: int = Field(description="Identifier of the policy assigned to the agent") - - -class DeletePolicyResponse(BaseModel): - success: bool = Field(description="Whether the policy was successfully removed") - - class AgentControlsResponse(BaseModel): controls: list[Control] = Field( - description="List of controls associated with the agent via its policy" + description="List of active controls associated with the agent" ) @@ -229,12 +220,14 @@ class AgentSummary(BaseModel): agent_id: str = Field(..., description="UUID of the agent") agent_name: str = Field(..., description="Human-readable name of the agent") - policy_id: int | None = Field(None, description="ID of assigned policy, if any") + policy_ids: list[int] = Field( + default_factory=list, description="IDs of policies associated with the agent" + ) 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") evaluator_count: int = Field(0, description="Number of evaluators registered with the agent") active_controls_count: int = Field( - 0, description="Number of active controls from agent's policy" + 0, description="Number of active controls for this agent" ) @@ -293,10 +286,14 @@ class DeleteControlResponse(BaseModel): """Response for deleting a control.""" success: bool = Field(..., description="Whether the control was deleted") - dissociated_from: list[int] = Field( + dissociated_from_policies: list[int] = Field( default_factory=list, description="Policy IDs the control was removed from before deletion", ) + dissociated_from_agents: list[str] = Field( + default_factory=list, + description="Agent IDs the control was removed from before deletion", + ) class PatchControlRequest(BaseModel): diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index be320f94..e8cc8358 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -660,7 +660,7 @@ async def list_agents( Returns: Dictionary containing: - agents: List of agent summaries with agent_id, agent_name, - policy_id, created_at, step_count, evaluator_count + policy_ids, created_at, step_count, evaluator_count - pagination: Object with limit, total, next_cursor, has_more Raises: @@ -689,6 +689,93 @@ async def main(): return await agents.list_agents(client, cursor=cursor, limit=limit) +# ============================================================================ +# Agent Association Convenience Functions +# ============================================================================ + + +async def get_agent_policies( + agent_id: str | UUID, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + List policy IDs associated with an agent. + """ + _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_policies(client, agent_id) + + +async def add_policy_to_agent( + agent_id: str | UUID, + policy_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + Associate a policy with an agent. + """ + _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.add_agent_policy(client, agent_id, policy_id) + + +async def remove_policy_from_agent( + agent_id: str | UUID, + policy_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + Remove a specific policy association from an agent. + """ + _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.remove_agent_policy_association(client, agent_id, policy_id) + + +async def clear_agent_policies( + agent_id: str | UUID, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + Remove all policy associations from an agent. + """ + _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.remove_agent_policies(client, agent_id) + + +async def add_control_to_agent( + agent_id: str | UUID, + control_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + Associate a control directly with an agent. + """ + _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.add_agent_control(client, agent_id, control_id) + + +async def remove_control_from_agent( + agent_id: str | UUID, + control_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """ + Remove a direct control association from an agent. + """ + _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.remove_agent_control(client, agent_id, control_id) + + # ============================================================================ # Control Management Convenience Functions # ============================================================================ @@ -869,7 +956,7 @@ async def delete_control( """ Delete a control from the server. - By default, deletion fails if the control is associated with any policy. + By default, deletion fails if the control is associated with any policy or agent. Use force=True to automatically dissociate and delete. Args: @@ -881,7 +968,8 @@ async def delete_control( Returns: Dictionary containing: - success: True if control was deleted - - dissociated_from: List of policy IDs the control was removed from + - dissociated_from_policies: List of policy IDs the control was removed from + - dissociated_from_agents: List of agent UUIDs the control was removed from Raises: httpx.HTTPError: If request fails @@ -895,7 +983,11 @@ async def delete_control( async def main(): # Force delete result = await agent_control.delete_control(5, force=True) - print(f"Deleted, removed from {len(result['dissociated_from'])} policies") + print( + "Deleted, removed from " + f"{len(result['dissociated_from_policies'])} policies and " + f"{len(result['dissociated_from_agents'])} agents" + ) asyncio.run(main()) """ @@ -1099,6 +1191,12 @@ async def main(): # Agent management "get_agent", "list_agents", + "get_agent_policies", + "add_policy_to_agent", + "remove_policy_from_agent", + "clear_agent_policies", + "add_control_to_agent", + "remove_control_from_agent", # Control management "create_control", "list_controls", diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index b7b3fd33..acdb3104 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -120,7 +120,7 @@ async def list_agents( Returns: Dictionary containing: - agents: List of agent summaries with agent_id, agent_name, - policy_id, created_at, step_count, evaluator_count + policy_ids, created_at, step_count, evaluator_count - pagination: Object with limit, total, next_cursor, has_more Raises: @@ -146,12 +146,12 @@ async def list_agents( return cast(dict[str, Any], response.json()) -async def get_agent_policy( +async def get_agent_policies( client: AgentControlClient, agent_id: str | UUID, ) -> dict[str, Any]: """ - Get the policy assigned to an agent. + Get policy IDs associated with an agent. Args: client: AgentControlClient instance @@ -159,28 +159,78 @@ async def get_agent_policy( Returns: Dictionary containing: - - policy_id: ID of the policy assigned to the agent + - policy_ids: IDs of policies associated with the agent Raises: - httpx.HTTPError: If request fails or agent has no policy + httpx.HTTPError: If request fails Example: async with AgentControlClient() as client: - policy = await get_agent_policy(client, agent_id) - print(f"Policy ID: {policy['policy_id']}") + policies = await get_agent_policies(client, agent_id) + print(f"Policy IDs: {policies['policy_ids']}") """ agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/policy") + response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/policies") response.raise_for_status() return cast(dict[str, Any], response.json()) -async def remove_agent_policy( +async def get_agent_policy( + client: AgentControlClient, + agent_id: str | UUID, +) -> dict[str, Any]: + """ + Backward-compatible alias for get_agent_policies(). + + Returns: + Dictionary containing: + - policy_ids: IDs of policies associated with the agent + """ + return await get_agent_policies(client, agent_id) + + +async def add_agent_policy( + client: AgentControlClient, + agent_id: str | UUID, + policy_id: int, +) -> dict[str, Any]: + """ + Associate a policy with an agent. + + This operation is idempotent. + """ + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.post( + f"/api/v1/agents/{agent_id_str}/policies/{policy_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_policy_association( client: AgentControlClient, agent_id: str | UUID, + policy_id: int, ) -> dict[str, Any]: """ - Remove the policy assignment from an agent. + Remove a specific policy association from an agent. + + This operation is idempotent. + """ + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.delete( + f"/api/v1/agents/{agent_id_str}/policies/{policy_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_policies( + client: AgentControlClient, + agent_id: str | UUID, +) -> dict[str, Any]: + """ + Remove all policy associations from an agent. Args: client: AgentControlClient instance @@ -190,9 +240,55 @@ async def remove_agent_policy( Dictionary containing success flag/details Raises: - httpx.HTTPError: If request fails or agent has no policy + httpx.HTTPError: If request fails + """ + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.delete(f"/api/v1/agents/{agent_id_str}/policies") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_policy( + client: AgentControlClient, + agent_id: str | UUID, +) -> dict[str, Any]: + """ + Backward-compatible alias for remove_agent_policies(). + """ + return await remove_agent_policies(client, agent_id) + + +async def add_agent_control( + client: AgentControlClient, + agent_id: str | UUID, + control_id: int, +) -> dict[str, Any]: + """ + Associate a control directly with an agent. + + This operation is idempotent. + """ + agent_id_str = ensure_uuid_str(agent_id) + response = await client.http_client.post( + f"/api/v1/agents/{agent_id_str}/controls/{control_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_control( + client: AgentControlClient, + agent_id: str | UUID, + control_id: int, +) -> dict[str, Any]: + """ + Remove a direct control association from an agent. + + This operation is idempotent. """ agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.delete(f"/api/v1/agents/{agent_id_str}/policy") + response = await client.http_client.delete( + f"/api/v1/agents/{agent_id_str}/controls/{control_id}" + ) 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..ff46974c 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -695,7 +695,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_id}/policies/{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/controls.py b/sdks/python/src/agent_control/controls.py index ea0457ec..4cf8f7ff 100644 --- a/sdks/python/src/agent_control/controls.py +++ b/sdks/python/src/agent_control/controls.py @@ -342,7 +342,7 @@ async def delete_control( """ Delete a control by ID. - By default, deletion fails if the control is associated with any policy. + By default, deletion fails if the control is associated with any policy or agent. Use force=True to automatically dissociate and delete. Args: @@ -353,7 +353,8 @@ async def delete_control( Returns: Dictionary containing: - success: True if control was deleted - - dissociated_from: List of policy IDs the control was removed from + - dissociated_from_policies: List of policy IDs the control was removed from + - dissociated_from_agents: List of agent UUIDs the control was removed from Raises: httpx.HTTPError: If request fails @@ -369,7 +370,11 @@ async def delete_control( if e.response.status_code == 409: # Force delete result = await delete_control(client, control_id=5, force=True) - print(f"Removed from {len(result['dissociated_from'])} policies") + print( + "Removed from " + f"{len(result['dissociated_from_policies'])} policies and " + f"{len(result['dissociated_from_agents'])} agents" + ) """ params = {"force": force} response = await client.http_client.delete( diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index dab7ecaa..b3266744 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -156,9 +156,9 @@ async def assign_policy_to_agent( policy_id: int ) -> dict[str, Any]: """ - Assign a policy to an agent. + Associate a policy with an agent. - This makes the policy active for the agent. Any existing policy assignment is replaced. + This operation is idempotent and additive: agents can be associated with many policies. Args: client: AgentControlClient instance @@ -174,7 +174,7 @@ async def assign_policy_to_agent( """ agent_id_str = ensure_uuid_str(agent_id) response = await client.http_client.post( - f"/api/v1/agents/{agent_id_str}/policy/{policy_id}" + f"/api/v1/agents/{agent_id_str}/policies/{policy_id}" ) response.raise_for_status() return cast(dict[str, Any], response.json()) diff --git a/sdks/typescript/overlays/method-names.overlay.yaml b/sdks/typescript/overlays/method-names.overlay.yaml index 3fd730c7..0907d56f 100644 --- a/sdks/typescript/overlays/method-names.overlay.yaml +++ b/sdks/typescript/overlays/method-names.overlay.yaml @@ -30,6 +30,16 @@ actions: x-speakeasy-group: agents x-speakeasy-name-override: listControls + - target: $["paths"]["/api/v1/agents/{agent_id}/controls/{control_id}"]["post"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: addControl + + - target: $["paths"]["/api/v1/agents/{agent_id}/controls/{control_id}"]["delete"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: removeControl + - target: $["paths"]["/api/v1/agents/{agent_id}/evaluators"]["get"] update: x-speakeasy-group: agents @@ -40,20 +50,25 @@ actions: x-speakeasy-group: agents x-speakeasy-name-override: getEvaluator - - target: $["paths"]["/api/v1/agents/{agent_id}/policy"]["get"] + - target: $["paths"]["/api/v1/agents/{agent_id}/policies"]["get"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: getPolicies + + - target: $["paths"]["/api/v1/agents/{agent_id}/policies"]["delete"] update: x-speakeasy-group: agents - x-speakeasy-name-override: getPolicy + x-speakeasy-name-override: removeAllAgentPolicies - - target: $["paths"]["/api/v1/agents/{agent_id}/policy"]["delete"] + - target: $["paths"]["/api/v1/agents/{agent_id}/policies/{policy_id}"]["post"] update: x-speakeasy-group: agents - x-speakeasy-name-override: deletePolicy + x-speakeasy-name-override: addPolicy - - target: $["paths"]["/api/v1/agents/{agent_id}/policy/{policy_id}"]["post"] + - target: $["paths"]["/api/v1/agents/{agent_id}/policies/{policy_id}"]["delete"] update: x-speakeasy-group: agents - x-speakeasy-name-override: updatePolicy + x-speakeasy-name-override: removePolicy - target: $["paths"]["/api/v1/controls"]["get"] update: diff --git a/sdks/typescript/src/generated/funcs/agents-add-control.ts b/sdks/typescript/src/generated/funcs/agents-add-control.ts new file mode 100644 index 00000000..8f889f87 --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-add-control.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Associate control directly with agent + * + * @remarks + * Associate a control directly with an agent (idempotent). + */ +export function agentsAddControl( + client: AgentControlSDKCore, + request: + operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_id: encodeSimple("agent_id", payload.agent_id, { + explode: false, + charEncoding: "percent", + }), + control_id: encodeSimple("control_id", payload.control_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_id}/controls/{control_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "add_agent_control_api_v1_agents__agent_id__controls__control_id__post", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "POST", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-update-policy.ts b/sdks/typescript/src/generated/funcs/agents-add-policy.ts similarity index 81% rename from sdks/typescript/src/generated/funcs/agents-update-policy.ts rename to sdks/typescript/src/generated/funcs/agents-add-policy.ts index ef8fbc68..c9f9d39e 100644 --- a/sdks/typescript/src/generated/funcs/agents-update-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-add-policy.ts @@ -28,32 +28,19 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Assign policy to agent + * Associate policy with agent * * @remarks - * 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 + * Associate a policy with an agent (idempotent). */ -export function agentsUpdatePolicy( +export function agentsAddPolicy( client: AgentControlSDKCore, - request: operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + request: + operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, options?: RequestOptions, ): APIPromise< Result< - models.SetPolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -74,12 +61,13 @@ export function agentsUpdatePolicy( async function $do( client: AgentControlSDKCore, - request: operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + request: + operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, options?: RequestOptions, ): Promise< [ Result< - models.SetPolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -98,7 +86,7 @@ async function $do( (value) => z.parse( operations - .SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest$outboundSchema, + .AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema, value, ), "Input validation failed", @@ -120,7 +108,7 @@ async function $do( }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy/{policy_id}")( + const path = pathToFunc("/api/v1/agents/{agent_id}/policies/{policy_id}")( pathParams, ); @@ -136,7 +124,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", + "add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -179,7 +167,7 @@ async function $do( }; const [result] = await M.match< - models.SetPolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -190,7 +178,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.SetPolicyResponse$inboundSchema), + M.json(200, models.AssocResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), diff --git a/sdks/typescript/src/generated/funcs/agents-get-policy.ts b/sdks/typescript/src/generated/funcs/agents-get-policies.ts similarity index 84% rename from sdks/typescript/src/generated/funcs/agents-get-policy.ts rename to sdks/typescript/src/generated/funcs/agents-get-policies.ts index ca3c7a92..6778b46e 100644 --- a/sdks/typescript/src/generated/funcs/agents-get-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-get-policies.ts @@ -28,28 +28,18 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Get agent's assigned policy + * List policies associated with agent * * @remarks - * 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 + * List policy IDs associated with an agent. */ -export function agentsGetPolicy( +export function agentsGetPolicies( client: AgentControlSDKCore, - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, options?: RequestOptions, ): APIPromise< Result< - models.GetPolicyResponse, + models.GetAgentPoliciesResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -70,12 +60,12 @@ export function agentsGetPolicy( async function $do( client: AgentControlSDKCore, - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, options?: RequestOptions, ): Promise< [ Result< - models.GetPolicyResponse, + models.GetAgentPoliciesResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -94,7 +84,7 @@ async function $do( (value) => z.parse( operations - .GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest$outboundSchema, + .GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema, value, ), "Input validation failed", @@ -112,7 +102,7 @@ async function $do( }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_id}/policies")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -125,7 +115,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_policies_api_v1_agents__agent_id__policies_get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -168,7 +158,7 @@ async function $do( }; const [result] = await M.match< - models.GetPolicyResponse, + models.GetAgentPoliciesResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -179,7 +169,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.GetPolicyResponse$inboundSchema), + M.json(200, models.GetAgentPoliciesResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), diff --git a/sdks/typescript/src/generated/funcs/agents-list-controls.ts b/sdks/typescript/src/generated/funcs/agents-list-controls.ts index 764e550c..8d517b8c 100644 --- a/sdks/typescript/src/generated/funcs/agents-list-controls.ts +++ b/sdks/typescript/src/generated/funcs/agents-list-controls.ts @@ -33,15 +33,14 @@ import { Result } from "../types/fp.js"; * @remarks * List all protection controls active for an agent. * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. + * Controls include the union of policy-derived and directly associated controls. * * Args: * agent_id: UUID of the agent * db: Database session (injected) * * Returns: - * AgentControlsResponse with list of controls (empty if no policy) + * AgentControlsResponse with list of active controls * * Raises: * HTTPException 404: Agent not found diff --git a/sdks/typescript/src/generated/funcs/agents-list.ts b/sdks/typescript/src/generated/funcs/agents-list.ts index 65d12ac0..72184635 100644 --- a/sdks/typescript/src/generated/funcs/agents-list.ts +++ b/sdks/typescript/src/generated/funcs/agents-list.ts @@ -33,7 +33,7 @@ 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 ID, name, policy associations, * and counts of registered steps and evaluators. * * Args: diff --git a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts b/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts similarity index 82% rename from sdks/typescript/src/generated/funcs/agents-delete-policy.ts rename to sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts index 9251bfd9..165f4bad 100644 --- a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts @@ -28,31 +28,19 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Remove agent's policy assignment + * Remove all policy associations from agent * * @remarks - * 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 + * Remove all policy associations from an agent. */ -export function agentsDeletePolicy( +export function agentsRemoveAllAgentPolicies( client: AgentControlSDKCore, - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, options?: RequestOptions, ): APIPromise< Result< - models.DeletePolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -73,12 +61,13 @@ export function agentsDeletePolicy( async function $do( client: AgentControlSDKCore, - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, options?: RequestOptions, ): Promise< [ Result< - models.DeletePolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -97,7 +86,7 @@ async function $do( (value) => z.parse( operations - .DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest$outboundSchema, + .RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema, value, ), "Input validation failed", @@ -115,7 +104,7 @@ async function $do( }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policy")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_id}/policies")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -128,7 +117,8 @@ 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: + "remove_all_agent_policies_api_v1_agents__agent_id__policies_delete", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -171,7 +161,7 @@ async function $do( }; const [result] = await M.match< - models.DeletePolicyResponse, + models.AssocResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -182,7 +172,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.DeletePolicyResponse$inboundSchema), + M.json(200, models.AssocResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), diff --git a/sdks/typescript/src/generated/funcs/agents-remove-control.ts b/sdks/typescript/src/generated/funcs/agents-remove-control.ts new file mode 100644 index 00000000..7005fd6e --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-remove-control.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Remove direct control association from agent + * + * @remarks + * Remove a direct control association from an agent (idempotent). + */ +export function agentsRemoveControl( + client: AgentControlSDKCore, + request: + operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_id: encodeSimple("agent_id", payload.agent_id, { + explode: false, + charEncoding: "percent", + }), + control_id: encodeSimple("control_id", payload.control_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_id}/controls/{control_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "DELETE", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts new file mode 100644 index 00000000..7c13890f --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Remove policy association from agent + * + * @remarks + * Remove a policy association from an agent (idempotent). + */ +export function agentsRemovePolicy( + client: AgentControlSDKCore, + request: + operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_id: encodeSimple("agent_id", payload.agent_id, { + explode: false, + charEncoding: "percent", + }), + policy_id: encodeSimple("policy_id", payload.policy_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_id}/policies/{policy_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "DELETE", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/controls-delete.ts b/sdks/typescript/src/generated/funcs/controls-delete.ts index c3b65d39..ea973a24 100644 --- a/sdks/typescript/src/generated/funcs/controls-delete.ts +++ b/sdks/typescript/src/generated/funcs/controls-delete.ts @@ -33,7 +33,7 @@ import { Result } from "../types/fp.js"; * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy. + * By default, deletion fails if the control is associated with any policy or agent. * Use force=true to automatically dissociate and delete. * * Args: @@ -42,7 +42,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and list of dissociated policies + * DeleteControlResponse with success flag and dissociation details * * Raises: * HTTPException 404: Control not found diff --git a/sdks/typescript/src/generated/models/agent-controls-response.ts b/sdks/typescript/src/generated/models/agent-controls-response.ts index 0668702c..22a591ee 100644 --- a/sdks/typescript/src/generated/models/agent-controls-response.ts +++ b/sdks/typescript/src/generated/models/agent-controls-response.ts @@ -10,7 +10,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; export type AgentControlsResponse = { /** - * List of controls associated with the agent via its policy + * List of active controls associated with the agent */ controls: Array; }; diff --git a/sdks/typescript/src/generated/models/agent-summary.ts b/sdks/typescript/src/generated/models/agent-summary.ts index 9915c153..4758c8d2 100644 --- a/sdks/typescript/src/generated/models/agent-summary.ts +++ b/sdks/typescript/src/generated/models/agent-summary.ts @@ -14,7 +14,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type AgentSummary = { /** - * Number of active controls from agent's policy + * Number of active controls for this agent */ activeControlsCount: number; /** @@ -34,9 +34,9 @@ export type AgentSummary = { */ evaluatorCount: number; /** - * ID of assigned policy, if any + * IDs of policies associated with the agent */ - policyId?: number | null | undefined; + policyIds?: Array | undefined; /** * Number of steps registered with the agent */ @@ -52,7 +52,7 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = agent_name: types.string(), created_at: z.optional(z.nullable(types.string())), evaluator_count: z._default(types.number(), 0), - policy_id: z.optional(z.nullable(types.number())), + policy_ids: types.optional(z.array(types.number())), step_count: z._default(types.number(), 0), }), z.transform((v) => { @@ -62,7 +62,7 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = "agent_name": "agentName", "created_at": "createdAt", "evaluator_count": "evaluatorCount", - "policy_id": "policyId", + "policy_ids": "policyIds", "step_count": "stepCount", }); }), diff --git a/sdks/typescript/src/generated/models/delete-control-response.ts b/sdks/typescript/src/generated/models/delete-control-response.ts index 623abd95..e57cbf83 100644 --- a/sdks/typescript/src/generated/models/delete-control-response.ts +++ b/sdks/typescript/src/generated/models/delete-control-response.ts @@ -13,10 +13,14 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; * Response for deleting a control. */ export type DeleteControlResponse = { + /** + * Agent IDs the control was removed from before deletion + */ + dissociatedFromAgents?: Array | undefined; /** * Policy IDs the control was removed from before deletion */ - dissociatedFrom?: Array | undefined; + dissociatedFromPolicies?: Array | undefined; /** * Whether the control was deleted */ @@ -29,12 +33,14 @@ export const DeleteControlResponse$inboundSchema: z.ZodMiniType< unknown > = z.pipe( z.object({ - dissociated_from: types.optional(z.array(types.number())), + dissociated_from_agents: types.optional(z.array(types.string())), + dissociated_from_policies: types.optional(z.array(types.number())), success: types.boolean(), }), z.transform((v) => { return remap$(v, { - "dissociated_from": "dissociatedFrom", + "dissociated_from_agents": "dissociatedFromAgents", + "dissociated_from_policies": "dissociatedFromPolicies", }); }), ); diff --git a/sdks/typescript/src/generated/models/delete-policy-response.ts b/sdks/typescript/src/generated/models/delete-policy-response.ts deleted file mode 100644 index d37ef18e..00000000 --- a/sdks/typescript/src/generated/models/delete-policy-response.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { safeParse } from "../lib/schemas.js"; -import { Result as SafeParseResult } from "../types/fp.js"; -import * as types from "../types/primitives.js"; -import { SDKValidationError } from "./errors/sdk-validation-error.js"; - -export type DeletePolicyResponse = { - /** - * Whether the policy was successfully removed - */ - success: boolean; -}; - -/** @internal */ -export const DeletePolicyResponse$inboundSchema: z.ZodMiniType< - DeletePolicyResponse, - unknown -> = z.object({ - success: types.boolean(), -}); - -export function deletePolicyResponseFromJSON( - jsonString: string, -): SafeParseResult { - return safeParse( - jsonString, - (x) => DeletePolicyResponse$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'DeletePolicyResponse' from JSON`, - ); -} diff --git a/sdks/typescript/src/generated/models/get-policy-response.ts b/sdks/typescript/src/generated/models/get-agent-policies-response.ts similarity index 51% rename from sdks/typescript/src/generated/models/get-policy-response.ts rename to sdks/typescript/src/generated/models/get-agent-policies-response.ts index fd8437df..3e9413e0 100644 --- a/sdks/typescript/src/generated/models/get-policy-response.ts +++ b/sdks/typescript/src/generated/models/get-agent-policies-response.ts @@ -9,34 +9,34 @@ import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; -export type GetPolicyResponse = { +export type GetAgentPoliciesResponse = { /** - * Identifier of the policy assigned to the agent + * IDs of policies associated with the agent */ - policyId: number; + policyIds?: Array | undefined; }; /** @internal */ -export const GetPolicyResponse$inboundSchema: z.ZodMiniType< - GetPolicyResponse, +export const GetAgentPoliciesResponse$inboundSchema: z.ZodMiniType< + GetAgentPoliciesResponse, unknown > = z.pipe( z.object({ - policy_id: types.number(), + policy_ids: types.optional(z.array(types.number())), }), z.transform((v) => { return remap$(v, { - "policy_id": "policyId", + "policy_ids": "policyIds", }); }), ); -export function getPolicyResponseFromJSON( +export function getAgentPoliciesResponseFromJSON( jsonString: string, -): SafeParseResult { +): SafeParseResult { return safeParse( jsonString, - (x) => GetPolicyResponse$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'GetPolicyResponse' from JSON`, + (x) => GetAgentPoliciesResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'GetAgentPoliciesResponse' from JSON`, ); } diff --git a/sdks/typescript/src/generated/models/index.ts b/sdks/typescript/src/generated/models/index.ts index 0fddcf5f..4582184f 100644 --- a/sdks/typescript/src/generated/models/index.ts +++ b/sdks/typescript/src/generated/models/index.ts @@ -26,7 +26,6 @@ export * from "./create-policy-request.js"; export * from "./create-policy-response.js"; export * from "./delete-control-response.js"; export * from "./delete-evaluator-config-response.js"; -export * from "./delete-policy-response.js"; export * from "./evaluation-request.js"; export * from "./evaluation-response.js"; export * from "./evaluator-config-item.js"; @@ -37,11 +36,11 @@ export * from "./evaluator-schema.js"; export * from "./evaluator-spec.js"; export * from "./event-query-request.js"; export * from "./event-query-response.js"; +export * from "./get-agent-policies-response.js"; export * from "./get-agent-response.js"; export * from "./get-control-data-response.js"; export * from "./get-control-response.js"; export * from "./get-policy-controls-response.js"; -export * from "./get-policy-response.js"; export * from "./health-response.js"; export * from "./init-agent-request.js"; export * from "./init-agent-response.js"; @@ -57,7 +56,6 @@ export * from "./patch-control-response.js"; export * from "./security.js"; export * from "./set-control-data-request.js"; export * from "./set-control-data-response.js"; -export * from "./set-policy-response.js"; export * from "./stats-response.js"; export * from "./stats-totals.js"; export * from "./step-key.js"; diff --git a/sdks/typescript/src/generated/models/init-agent-response.ts b/sdks/typescript/src/generated/models/init-agent-response.ts index cca583a9..bf72e07c 100644 --- a/sdks/typescript/src/generated/models/init-agent-response.ts +++ b/sdks/typescript/src/generated/models/init-agent-response.ts @@ -14,7 +14,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type InitAgentResponse = { /** - * Active protection controls for the agent (if policy assigned) + * Active protection controls for the agent */ controls?: Array | undefined; /** diff --git a/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts new file mode 100644 index 00000000..320fddef --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-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 AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest = { + agentId: string; + controlId: number; +}; + +/** @internal */ +export type AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$Outbound = + { + agent_id: string; + control_id: number; + }; + +/** @internal */ +export const AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema: + z.ZodMiniType< + AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$Outbound, + AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest + > = z.pipe( + z.object({ + agentId: z.string(), + controlId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + controlId: "control_id", + }); + }), + ); + +export function addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequestToJSON( + addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest: + AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, +): string { + return JSON.stringify( + AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema + .parse(addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts new file mode 100644 index 00000000..bbc38700 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-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 AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest = { + agentId: string; + policyId: number; +}; + +/** @internal */ +export type AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$Outbound = + { + agent_id: string; + policy_id: number; + }; + +/** @internal */ +export const AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema: + z.ZodMiniType< + AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$Outbound, + AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest + > = z.pipe( + z.object({ + agentId: z.string(), + policyId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + policyId: "policy_id", + }); + }), + ); + +export function addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequestToJSON( + addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest: + AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, +): string { + return JSON.stringify( + AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema + .parse(addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest), + ); +} 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-control-api-v1-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts index 6d3935ba..b016ec01 100644 --- a/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts +++ b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts @@ -8,7 +8,7 @@ import { remap as remap$ } from "../../lib/primitives.js"; export type DeleteControlApiV1ControlsControlIdDeleteRequest = { controlId: number; /** - * If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. + * If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ force?: boolean | undefined; }; diff --git a/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-get.ts new file mode 100644 index 00000000..94a18506 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-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 GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest = { + agentId: string; +}; + +/** @internal */ +export type GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$Outbound = { + agent_id: string; +}; + +/** @internal */ +export const GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema: + z.ZodMiniType< + GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$Outbound, + GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest + > = z.pipe( + z.object({ + agentId: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + }); + }), + ); + +export function getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequestToJSON( + getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest: + GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, +): string { + return JSON.stringify( + GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema.parse( + getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, + ), + ); +} 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/index.ts b/sdks/typescript/src/generated/models/operations/index.ts index 2a322b0f..96194f2b 100644 --- a/sdks/typescript/src/generated/models/operations/index.ts +++ b/sdks/typescript/src/generated/models/operations/index.ts @@ -2,14 +2,15 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +export * from "./add-agent-control-api-v1-agents-agent-id-controls-control-id-post.js"; +export * from "./add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.js"; 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-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-policies-api-v1-agents-agent-id-policies-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"; @@ -23,7 +24,9 @@ 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-control-api-v1-controls-control-id-patch.js"; +export * from "./remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.js"; +export * from "./remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.js"; +export * from "./remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.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-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/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts new file mode 100644 index 00000000..8d53fc48 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.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 RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest = + { + agentId: string; + controlId: number; + }; + +/** @internal */ +export type RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$Outbound = + { + agent_id: string; + control_id: number; + }; + +/** @internal */ +export const RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$Outbound, + RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest + > = z.pipe( + z.object({ + agentId: z.string(), + controlId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + controlId: "control_id", + }); + }), + ); + +export function removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequestToJSON( + removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest: + RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, +): string { + return JSON.stringify( + RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema + .parse( + removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts new file mode 100644 index 00000000..ac14a1ad --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.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 RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest = { + agentId: string; + policyId: number; +}; + +/** @internal */ +export type RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$Outbound = + { + agent_id: string; + policy_id: number; + }; + +/** @internal */ +export const RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$Outbound, + RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest + > = z.pipe( + z.object({ + agentId: z.string(), + policyId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + policyId: "policy_id", + }); + }), + ); + +export function removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequestToJSON( + removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest: + RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, +): string { + return JSON.stringify( + RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema + .parse(removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts b/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts new file mode 100644 index 00000000..59a64b75 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-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 RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest = { + agentId: string; +}; + +/** @internal */ +export type RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$Outbound = + { + agent_id: string; + }; + +/** @internal */ +export const RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$Outbound, + RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest + > = z.pipe( + z.object({ + agentId: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentId: "agent_id", + }); + }), + ); + +export function removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequestToJSON( + removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest: + RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, +): string { + return JSON.stringify( + RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema + .parse(removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest), + ); +} 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/set-policy-response.ts b/sdks/typescript/src/generated/models/set-policy-response.ts deleted file mode 100644 index 7fa3bfe6..00000000 --- a/sdks/typescript/src/generated/models/set-policy-response.ts +++ /dev/null @@ -1,47 +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 { safeParse } from "../lib/schemas.js"; -import { Result as SafeParseResult } from "../types/fp.js"; -import * as types from "../types/primitives.js"; -import { SDKValidationError } from "./errors/sdk-validation-error.js"; - -export type SetPolicyResponse = { - /** - * Previous policy id if one was replaced - */ - oldPolicyId?: number | null | undefined; - /** - * Whether the policy was successfully assigned - */ - success: boolean; -}; - -/** @internal */ -export const SetPolicyResponse$inboundSchema: z.ZodMiniType< - SetPolicyResponse, - unknown -> = z.pipe( - z.object({ - old_policy_id: z.optional(z.nullable(types.number())), - success: types.boolean(), - }), - z.transform((v) => { - return remap$(v, { - "old_policy_id": "oldPolicyId", - }); - }), -); - -export function setPolicyResponseFromJSON( - jsonString: string, -): SafeParseResult { - return safeParse( - jsonString, - (x) => SetPolicyResponse$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'SetPolicyResponse' from JSON`, - ); -} diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index 4d38ab54..b1e0d014 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -2,15 +2,18 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { agentsDeletePolicy } from "../funcs/agents-delete-policy.js"; +import { agentsAddControl } from "../funcs/agents-add-control.js"; +import { agentsAddPolicy } from "../funcs/agents-add-policy.js"; import { agentsGetEvaluator } from "../funcs/agents-get-evaluator.js"; -import { agentsGetPolicy } from "../funcs/agents-get-policy.js"; +import { agentsGetPolicies } from "../funcs/agents-get-policies.js"; import { agentsGet } from "../funcs/agents-get.js"; import { agentsInit } from "../funcs/agents-init.js"; import { agentsListControls } from "../funcs/agents-list-controls.js"; import { agentsListEvaluators } from "../funcs/agents-list-evaluators.js"; import { agentsList } from "../funcs/agents-list.js"; -import { agentsUpdatePolicy } from "../funcs/agents-update-policy.js"; +import { agentsRemoveAllAgentPolicies } from "../funcs/agents-remove-all-agent-policies.js"; +import { agentsRemoveControl } from "../funcs/agents-remove-control.js"; +import { agentsRemovePolicy } from "../funcs/agents-remove-policy.js"; import { agentsUpdate } from "../funcs/agents-update.js"; import { ClientSDK, RequestOptions } from "../lib/sdks.js"; import * as models from "../models/index.js"; @@ -24,7 +27,7 @@ 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 ID, name, policy associations, * and counts of registered steps and evaluators. * * Args: @@ -152,15 +155,14 @@ export class Agents extends ClientSDK { * @remarks * List all protection controls active for an agent. * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. + * Controls include the union of policy-derived and directly associated controls. * * Args: * agent_id: UUID of the agent * db: Database session (injected) * * Returns: - * AgentControlsResponse with list of controls (empty if no policy) + * AgentControlsResponse with list of active controls * * Raises: * HTTPException 404: Agent not found @@ -176,6 +178,42 @@ export class Agents extends ClientSDK { )); } + /** + * Remove direct control association from agent + * + * @remarks + * Remove a direct control association from an agent (idempotent). + */ + async removeControl( + request: + operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsRemoveControl( + this, + request, + options, + )); + } + + /** + * Associate control directly with agent + * + * @remarks + * Associate a control directly with an agent (idempotent). + */ + async addControl( + request: + operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsAddControl( + this, + request, + options, + )); + } + /** * List agent's registered evaluator schemas * @@ -240,29 +278,17 @@ export class Agents extends ClientSDK { } /** - * Remove agent's policy assignment + * Remove all policy associations from agent * * @remarks - * 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 + * Remove all policy associations from an agent. */ - async deletePolicy( - request: operations.DeleteAgentPolicyApiV1AgentsAgentIdPolicyDeleteRequest, + async removeAllAgentPolicies( + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsDeletePolicy( + ): Promise { + return unwrapAsync(agentsRemoveAllAgentPolicies( this, request, options, @@ -270,26 +296,16 @@ export class Agents extends ClientSDK { } /** - * Get agent's assigned policy + * List policies associated with agent * * @remarks - * 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 + * List policy IDs associated with an agent. */ - async getPolicy( - request: operations.GetAgentPolicyApiV1AgentsAgentIdPolicyGetRequest, + async getPolicies( + request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsGetPolicy( + ): Promise { + return unwrapAsync(agentsGetPolicies( this, request, options, @@ -297,31 +313,35 @@ export class Agents extends ClientSDK { } /** - * Assign policy to agent + * Remove policy association from agent * * @remarks - * 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) + * Remove a policy association from an agent (idempotent). + */ + async removePolicy( + request: + operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsRemovePolicy( + this, + request, + options, + )); + } + + /** + * Associate policy with agent * - * Raises: - * HTTPException 404: Agent or policy not found - * HTTPException 500: Database error during assignment + * @remarks + * Associate a policy with an agent (idempotent). */ - async updatePolicy( + async addPolicy( request: - operations.SetAgentPolicyApiV1AgentsAgentIdPolicyPolicyIdPostRequest, + operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsUpdatePolicy( + ): Promise { + return unwrapAsync(agentsAddPolicy( this, request, options, diff --git a/sdks/typescript/src/generated/sdk/controls.ts b/sdks/typescript/src/generated/sdk/controls.ts index e4871a8a..c32be744 100644 --- a/sdks/typescript/src/generated/sdk/controls.ts +++ b/sdks/typescript/src/generated/sdk/controls.ts @@ -113,7 +113,7 @@ export class Controls extends ClientSDK { * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy. + * By default, deletion fails if the control is associated with any policy or agent. * Use force=true to automatically dissociate and delete. * * Args: @@ -122,7 +122,7 @@ export class Controls extends ClientSDK { * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and list of dissociated policies + * DeleteControlResponse with success flag and dissociation details * * Raises: * HTTPException 404: Control not found diff --git a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py new file mode 100644 index 00000000..e01d55a2 --- /dev/null +++ b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py @@ -0,0 +1,80 @@ +""" +Revision ID: 58e519e06e02 +Revises: d2f4a6b8c9d0 +Create Date: 2026-02-23 13:39:21.295377 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '58e519e06e02' +down_revision = 'd2f4a6b8c9d0' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('agent_controls', + sa.Column('agent_uuid', sa.UUID(), nullable=False), + sa.Column('control_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['agent_uuid'], ['agents.agent_uuid'], ), + sa.ForeignKeyConstraint(['control_id'], ['controls.id'], ), + sa.PrimaryKeyConstraint('agent_uuid', 'control_id') + ) + op.create_index(op.f('ix_agent_controls_agent_uuid'), 'agent_controls', ['agent_uuid'], unique=False) + op.create_index(op.f('ix_agent_controls_control_id'), 'agent_controls', ['control_id'], unique=False) + op.create_table('agent_policies', + sa.Column('agent_uuid', sa.UUID(), nullable=False), + sa.Column('policy_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['agent_uuid'], ['agents.agent_uuid'], ), + sa.ForeignKeyConstraint(['policy_id'], ['policies.id'], ), + sa.PrimaryKeyConstraint('agent_uuid', 'policy_id') + ) + op.create_index(op.f('ix_agent_policies_agent_uuid'), 'agent_policies', ['agent_uuid'], unique=False) + op.create_index(op.f('ix_agent_policies_policy_id'), 'agent_policies', ['policy_id'], unique=False) + op.execute( + sa.text( + """ + INSERT INTO agent_policies (agent_uuid, policy_id) + SELECT agent_uuid, policy_id + FROM agents + WHERE policy_id IS NOT NULL + ON CONFLICT (agent_uuid, policy_id) DO NOTHING + """ + ) + ) + op.drop_index(op.f('ix_agents_policy_id'), table_name='agents') + op.drop_constraint(op.f('agents_policy_id_fkey'), 'agents', type_='foreignkey') + op.drop_column('agents', 'policy_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('agents', sa.Column('policy_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.execute( + sa.text( + """ + UPDATE agents AS a + SET policy_id = ap.policy_id + FROM ( + SELECT agent_uuid, MIN(policy_id) AS policy_id + FROM agent_policies + GROUP BY agent_uuid + ) AS ap + WHERE a.agent_uuid = ap.agent_uuid + """ + ) + ) + op.create_foreign_key(op.f('agents_policy_id_fkey'), 'agents', 'policies', ['policy_id'], ['id']) + op.create_index(op.f('ix_agents_policy_id'), 'agents', ['policy_id'], unique=False) + op.drop_index(op.f('ix_agent_policies_policy_id'), table_name='agent_policies') + op.drop_index(op.f('ix_agent_policies_agent_uuid'), table_name='agent_policies') + op.drop_table('agent_policies') + op.drop_index(op.f('ix_agent_controls_control_id'), table_name='agent_controls') + op.drop_index(op.f('ix_agent_controls_agent_uuid'), table_name='agent_controls') + op.drop_table('agent_controls') + # ### end Alembic commands ### diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 4d99e414..c074e759 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -8,23 +8,22 @@ from agent_control_models.server import ( AgentControlsResponse, AgentSummary, - DeletePolicyResponse, + AssocResponse, EvaluatorSchema, + GetAgentPoliciesResponse, GetAgentResponse, - GetPolicyResponse, InitAgentRequest, InitAgentResponse, ListAgentsResponse, PaginationInfo, PatchAgentRequest, PatchAgentResponse, - SetPolicyResponse, StepKey, ) from fastapi import APIRouter, Depends from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import BaseModel, ValidationError -from sqlalchemy import func, or_, select +from sqlalchemy import delete, func, or_, select, union_all from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -41,6 +40,8 @@ AgentData, Control, Policy, + agent_controls, + agent_policies, policy_controls, ) from ..services.controls import list_controls_for_agent, list_controls_for_policy @@ -82,18 +83,8 @@ def _get_builtin_evaluator_names() -> set[str]: return _BUILTIN_EVALUATOR_NAMES -async def _validate_policy_controls_for_agent( - agent: Agent, policy_id: int, db: AsyncSession -) -> list[str]: - """Validate all controls in a policy can run on this agent. - - Checks that agent-scoped evaluators referenced by controls: - 1. Exist on the agent (registered via initAgent) - 2. Have config that validates against the evaluator's schema - - Returns: - List of error messages (empty if all valid) - """ +def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[str]: + """Validate controls can run on this agent.""" errors: list[str] = [] # Parse agent's registered evaluators @@ -104,9 +95,6 @@ async def _validate_policy_controls_for_agent( agent_evaluators = {e.name: e for e in (agent_data.evaluators or [])} - # Get all controls for this policy - controls = await list_controls_for_policy(policy_id, db) - for control in controls: if not control.data: continue @@ -152,6 +140,14 @@ async def _validate_policy_controls_for_agent( return errors +async def _validate_policy_controls_for_agent( + agent: Agent, policy_id: int, db: AsyncSession +) -> list[str]: + """Validate all controls in a policy can run on this agent.""" + controls = await list_controls_for_policy(policy_id, db) + return _validate_controls_for_agent(agent, controls) + + @router.get( "", response_model=ListAgentsResponse, @@ -167,7 +163,7 @@ 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 ID, name, policy associations, and counts of registered steps and evaluators. Args: @@ -237,32 +233,54 @@ async def list_agents( if has_more and agents: next_cursor = str(agents[-1].agent_uuid) - # 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 + # Batch query: Get policy IDs and active control counts for all agents at once control_counts_map: dict[UUID, int] = {} + policy_ids_map: dict[UUID, list[int]] = {} if agents: + agent_uuids = [agent.agent_uuid for agent in agents] + + # Policy associations per agent + policy_ids_query = ( + select(agent_policies.c.agent_uuid, agent_policies.c.policy_id) + .where(agent_policies.c.agent_uuid.in_(agent_uuids)) + .order_by(agent_policies.c.agent_uuid, agent_policies.c.policy_id) + ) + policy_ids_result = await db.execute(policy_ids_query) + for agent_uuid, policy_id in policy_ids_result.all(): + policy_ids_map.setdefault(agent_uuid, []).append(policy_id) + + # Active controls per agent (policy-derived + direct controls, de-duplicated) + policy_associations = ( + select( + agent_policies.c.agent_uuid.label("agent_uuid"), + policy_controls.c.control_id.label("control_id"), + ) + .select_from( + agent_policies.join( + policy_controls, agent_policies.c.policy_id == policy_controls.c.policy_id + ) + ) + .where(agent_policies.c.agent_uuid.in_(agent_uuids)) + ) + direct_associations = select( + agent_controls.c.agent_uuid.label("agent_uuid"), + agent_controls.c.control_id.label("control_id"), + ).where(agent_controls.c.agent_uuid.in_(agent_uuids)) + all_associations = union_all(policy_associations, direct_associations).subquery() + control_counts_query = ( select( - Agent.agent_uuid, - func.count(func.distinct(policy_controls.c.control_id)).label("count"), + all_associations.c.agent_uuid, + func.count(func.distinct(all_associations.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) + .join(Control, all_associations.c.control_id == Control.id) .where( - Agent.agent_uuid.in_([agent.agent_uuid 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) or_( Control.data["enabled"].astext == "true", ~Control.data.has_key("enabled"), ), ) - .group_by(Agent.agent_uuid) + .group_by(all_associations.c.agent_uuid) ) control_counts_result = await db.execute(control_counts_query) control_counts_map = {row[0]: row[1] for row in control_counts_result.all()} @@ -289,7 +307,7 @@ async def list_agents( AgentSummary( agent_id=str(agent.agent_uuid), agent_name=agent.name, - policy_id=agent.policy_id, + policy_ids=policy_ids_map.get(agent.agent_uuid, []), created_at=agent.created_at.isoformat() if agent.created_at else None, step_count=step_count, evaluator_count=evaluator_count, @@ -644,10 +662,8 @@ async def init_agent( operation="update", ) - # 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) + # Include all active controls (policy-derived and direct). + controls = await list_controls_for_agent(existing.agent_uuid, db) return InitAgentResponse(created=created, controls=controls) @@ -724,33 +740,8 @@ async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> ) -@router.post( - "/{agent_id}/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) -) -> SetPolicyResponse: - """ - 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 - """ - # Find agent +async def _get_agent_or_404(agent_id: UUID, db: AsyncSession) -> Agent: + """Get an agent or raise AGENT_NOT_FOUND.""" result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) agent: Agent | None = result.scalars().first() if agent is None: @@ -761,8 +752,21 @@ async def set_agent_policy( resource_id=str(agent_id), hint="Verify the agent ID is correct and the agent has been registered.", ) + return agent + + +@router.post( + "/{agent_id}/policies/{policy_id}", + response_model=AssocResponse, + summary="Associate policy with agent", + response_description="Success confirmation", +) +async def add_agent_policy( + agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Associate a policy with an agent (idempotent).""" + agent = await _get_agent_or_404(agent_id, db) - # Find policy by id policy_result = await db.execute(select(Policy).where(Policy.id == policy_id)) policy: Policy | None = policy_result.scalars().first() if policy is None: @@ -774,7 +778,6 @@ async def set_agent_policy( hint="Verify the policy ID is correct and the policy has been created.", ) - # Validate controls can run on this agent validation_errors = await _validate_policy_controls_for_agent(agent, policy_id, db) if validation_errors: raise BadRequestError( @@ -792,161 +795,251 @@ async def set_agent_policy( ], ) - # Store old policy ID if exists - old_policy_id: int | None = None - if agent.policy_id is not None: - old_policy_id = agent.policy_id - - # Assign new policy - agent.policy_id = policy.id try: + from sqlalchemy.dialects.postgresql import insert as pg_insert + + stmt = ( + pg_insert(agent_policies) + .values(agent_uuid=agent_id, policy_id=policy_id) + .on_conflict_do_nothing() + ) + await db.execute(stmt) await db.commit() except Exception: await db.rollback() _logger.error( - f"Failed to assign policy '{policy_id}' to agent '{agent.name}' ({agent_id})", + "Failed to associate policy '%s' with agent '%s' (%s)", + policy_id, + agent.name, + agent_id, exc_info=True, ) raise DatabaseError( - detail=f"Failed to assign policy to agent '{agent.name}': database error", + detail=f"Failed to associate policy with agent '{agent.name}': database error", resource="Agent", - operation="assign policy", + operation="add policy association", ) - return SetPolicyResponse(success=True, old_policy_id=old_policy_id) + return AssocResponse(success=True) @router.get( - "/{agent_id}/policy", - response_model=GetPolicyResponse, - summary="Get agent's assigned policy", - response_description="Policy ID", + "/{agent_id}/policies", + response_model=GetAgentPoliciesResponse, + summary="List policies associated with agent", + response_description="List of policy IDs", ) -async def get_agent_policy( +async def get_agent_policies( agent_id: UUID, db: AsyncSession = Depends(get_async_db) -) -> GetPolicyResponse: - """ - Retrieve the policy currently assigned to an agent. - - Args: - agent_id: UUID of the agent - db: Database session (injected) +) -> GetAgentPoliciesResponse: + """List policy IDs associated with an agent.""" + await _get_agent_or_404(agent_id, db) + result = await db.execute( + select(agent_policies.c.policy_id) + .where(agent_policies.c.agent_uuid == agent_id) + .order_by(agent_policies.c.policy_id) + ) + return GetAgentPoliciesResponse(policy_ids=[row[0] for row in result.all()]) - Returns: - GetPolicyResponse with policy ID - Raises: - HTTPException 404: Agent not found or agent has no policy assigned - """ - # Find agent - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) - 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", - resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", - ) +@router.delete( + "/{agent_id}/policies/{policy_id}", + response_model=AssocResponse, + summary="Remove policy association from agent", + response_description="Success confirmation", +) +async def remove_agent_policy( + agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Remove a policy association from an agent (idempotent).""" + await _get_agent_or_404(agent_id, db) - # Check if agent has a policy - if agent.policy_id is None: + policy_result = await db.execute(select(Policy.id).where(Policy.id == policy_id)) + if policy_result.first() is None: raise NotFoundError( error_code=ErrorCode.POLICY_NOT_FOUND, - detail=f"Agent '{agent.name}' has no policy assigned", + detail=f"Policy with ID '{policy_id}' not found", resource="Policy", - hint="Assign a policy to the agent using POST /{agent_id}/policy/{policy_id}.", + resource_id=str(policy_id), + hint="Verify the policy ID is correct and the policy has been created.", ) - # Find policy - policy_result = await db.execute(select(Policy).where(Policy.id == agent.policy_id)) - policy: Policy | None = policy_result.scalars().first() - if policy is None: - raise NotFoundError( - error_code=ErrorCode.POLICY_NOT_FOUND, - detail=( - f"Policy with ID '{agent.policy_id}' not found " - f"(referenced by agent '{agent.name}')" - ), - resource="Policy", - resource_id=str(agent.policy_id), - hint="The referenced policy may have been deleted. Assign a new policy to the agent.", + try: + await db.execute( + delete(agent_policies).where( + (agent_policies.c.agent_uuid == agent_id) + & (agent_policies.c.policy_id == policy_id) + ) + ) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to remove policy '%s' from agent '%s'", + policy_id, + agent_id, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to remove policy association from agent '{agent_id}': database error", + resource="Agent", + operation="remove policy association", ) - return GetPolicyResponse(policy_id=policy.id) + return AssocResponse(success=True) @router.delete( - "/{agent_id}/policy", - response_model=DeletePolicyResponse, - summary="Remove agent's policy assignment", + "/{agent_id}/policies", + response_model=AssocResponse, + summary="Remove all policy associations from agent", response_description="Success confirmation", ) -async def delete_agent_policy( +async def remove_all_agent_policies( agent_id: UUID, db: AsyncSession = Depends(get_async_db) -) -> DeletePolicyResponse: - """ - Remove the policy assignment from an agent. +) -> AssocResponse: + """Remove all policy associations from an agent.""" + await _get_agent_or_404(agent_id, db) - The agent will no longer have any protection controls active. + try: + await db.execute(delete(agent_policies).where(agent_policies.c.agent_uuid == agent_id)) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to remove all policies from agent '%s'", + agent_id, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to remove policy associations from agent '{agent_id}': database error", + resource="Agent", + operation="remove all policy associations", + ) - Args: - agent_id: UUID of the agent - db: Database session (injected) + return AssocResponse(success=True) - Returns: - DeletePolicyResponse with success flag - Raises: - HTTPException 404: Agent not found or agent has no policy assigned - HTTPException 500: Database error during removal - """ - # Find agent - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) - agent: Agent | None = result.scalars().first() - if agent is None: +@router.post( + "/{agent_id}/controls/{control_id}", + response_model=AssocResponse, + summary="Associate control directly with agent", + response_description="Success confirmation", +) +async def add_agent_control( + agent_id: UUID, control_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Associate a control directly with an agent (idempotent).""" + agent = await _get_agent_or_404(agent_id, db) + + control_result = await db.execute(select(Control).where(Control.id == control_id)) + control: Control | None = control_result.scalars().first() + if control is None: raise NotFoundError( - error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with ID '{agent_id}' not found", + error_code=ErrorCode.CONTROL_NOT_FOUND, + detail=f"Control with ID '{control_id}' not found", + resource="Control", + resource_id=str(control_id), + hint="Verify the control ID is correct and the control has been created.", + ) + + validation_errors = _validate_controls_for_agent(agent, [control]) + if validation_errors: + raise BadRequestError( + error_code=ErrorCode.POLICY_CONTROL_INCOMPATIBLE, + detail="Control is incompatible with this agent", + hint="Ensure the control is compatible with this agent's evaluators.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator", + code="incompatible", + message=err, + ) + for err in validation_errors + ], + ) + + try: + from sqlalchemy.dialects.postgresql import insert as pg_insert + + stmt = ( + pg_insert(agent_controls) + .values(agent_uuid=agent_id, control_id=control_id) + .on_conflict_do_nothing() + ) + await db.execute(stmt) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to associate control '%s' with agent '%s' (%s)", + control_id, + agent.name, + agent_id, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to associate control with agent '{agent.name}': database error", resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", + operation="add control association", ) - # Check if agent has a policy - if agent.policy_id is None: + return AssocResponse(success=True) + + +@router.delete( + "/{agent_id}/controls/{control_id}", + response_model=AssocResponse, + summary="Remove direct control association from agent", + response_description="Success confirmation", +) +async def remove_agent_control( + agent_id: UUID, control_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Remove a direct control association from an agent (idempotent).""" + await _get_agent_or_404(agent_id, db) + + control_result = await db.execute(select(Control.id).where(Control.id == control_id)) + if control_result.first() is None: raise NotFoundError( - error_code=ErrorCode.POLICY_NOT_FOUND, - detail=f"Agent '{agent.name}' has no policy assigned", - resource="Policy", - hint="The agent does not have a policy to remove.", + error_code=ErrorCode.CONTROL_NOT_FOUND, + detail=f"Control with ID '{control_id}' not found", + resource="Control", + resource_id=str(control_id), + hint="Verify the control ID is correct and the control has been created.", ) - # Remove policy assignment - agent.policy_id = None try: + await db.execute( + delete(agent_controls).where( + (agent_controls.c.agent_uuid == agent_id) + & (agent_controls.c.control_id == control_id) + ) + ) await db.commit() except Exception: await db.rollback() _logger.error( - f"Failed to remove policy from agent '{agent.name}' ({agent_id})", + "Failed to remove control '%s' from agent '%s'", + control_id, + agent_id, exc_info=True, ) raise DatabaseError( - detail=f"Failed to remove policy from agent '{agent.name}': database error", + detail=f"Failed to remove control association from agent '{agent_id}': database error", resource="Agent", - operation="remove policy", + operation="remove control association", ) - return DeletePolicyResponse(success=True) + return AssocResponse(success=True) @router.get( "/{agent_id}/controls", response_model=AgentControlsResponse, summary="List agent's active controls", - response_description="List of controls from agent's policy", + response_description="List of controls from agent policy and direct associations", ) async def list_agent_controls( agent_id: UUID, db: AsyncSession = Depends(get_async_db) @@ -954,32 +1047,19 @@ async def list_agent_controls( """ List all protection controls active for an agent. - Controls are inherited from the agent's assigned policy. - Returns an empty list if the agent has no policy. + Controls include the union of policy-derived and directly associated controls. Args: agent_id: UUID of the agent db: Database session (injected) Returns: - AgentControlsResponse with list of controls (empty if no policy) + AgentControlsResponse with list of active controls Raises: HTTPException 404: Agent not found """ - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) - 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", - resource="Agent", - resource_id=str(agent_id), - hint="Verify the agent ID is correct and the agent has been registered.", - ) - - if agent.policy_id is None: - return AgentControlsResponse(controls=[]) + await _get_agent_or_404(agent_id, db) controls = await list_controls_for_agent(agent_id, db) return AgentControlsResponse(controls=controls) @@ -1237,37 +1317,35 @@ async def patch_agent( if request.remove_evaluators: remove_evaluator_set = set(request.remove_evaluators) - # 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) - referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) - - for ctrl in controls: - evaluator_ref = ctrl.control.evaluator.name - if ":" in evaluator_ref: - ref_agent, ref_eval = evaluator_ref.split(":", 1) - # Check if this control references an evaluator we're removing - # AND it's scoped to this agent (by name match) - if ref_agent == agent.name and ref_eval in remove_evaluator_set: - referencing_controls.append((ctrl.name, ref_eval)) - - if referencing_controls: - raise ConflictError( - error_code=ErrorCode.EVALUATOR_IN_USE, - detail="Cannot remove evaluators: active controls reference them", - resource="Evaluator", - hint="Remove or update the controls that reference these evaluators first.", - errors=[ - ValidationErrorItem( - resource="Control", - field="evaluator.name", - code="in_use", - message=f"Control '{ctrl}' uses evaluator '{ev}'", - ) - for ctrl, ev in referencing_controls - ], - ) + # Check if any active controls reference evaluators being removed + controls = await list_controls_for_agent(agent.agent_uuid, db) + referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) + + for ctrl in controls: + evaluator_ref = ctrl.control.evaluator.name + if ":" in evaluator_ref: + ref_agent, ref_eval = evaluator_ref.split(":", 1) + # Check if this control references an evaluator we're removing + # AND it's scoped to this agent (by name match) + if ref_agent == agent.name and ref_eval in remove_evaluator_set: + referencing_controls.append((ctrl.name, ref_eval)) + + if referencing_controls: + raise ConflictError( + error_code=ErrorCode.EVALUATOR_IN_USE, + detail="Cannot remove evaluators: active controls reference them", + resource="Evaluator", + hint="Remove or update the controls that reference these evaluators first.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator.name", + code="in_use", + message=f"Control '{ctrl}' uses evaluator '{ev}'", + ) + for ctrl, ev in referencing_controls + ], + ) new_evaluators = [] for ev in data_model.evaluators or []: diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index e4977120..0cf96497 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -21,7 +21,7 @@ from fastapi import APIRouter, Depends, Query from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import ValidationError -from sqlalchemy import delete, func, or_, select +from sqlalchemy import delete, func, or_, select, union_all from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -32,7 +32,7 @@ NotFoundError, ) from ..logging_utils import get_logger -from ..models import Agent, AgentData, Control, Policy, policy_controls +from ..models import Agent, AgentData, Control, agent_controls, agent_policies, policy_controls from ..services.evaluator_utils import ( parse_evaluator_ref_full, validate_config_against_schema, @@ -594,21 +594,34 @@ async def list_controls( controls = controls[:-1] # Build mapping of control_id -> agent that uses it - # Traversal: Control -> policy_controls -> Policy -> Agent + # Traversal includes both: + # - Control -> policy_controls -> agent_policies -> Agent + # - Control -> agent_controls -> Agent control_agent_map: dict[int, AgentRef | None] = {ctrl.id: None for ctrl in controls} if controls: control_ids = [ctrl.id for ctrl in controls] - agents_query = ( + policy_agents_query = ( select( policy_controls.c.control_id, - Agent.agent_uuid, + agent_policies.c.agent_uuid, Agent.name, ) .select_from(policy_controls) - .join(Policy, policy_controls.c.policy_id == Policy.id) - .join(Agent, Agent.policy_id == Policy.id) + .join(agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id) + .join(Agent, Agent.agent_uuid == agent_policies.c.agent_uuid) .where(policy_controls.c.control_id.in_(control_ids)) ) + direct_agents_query = ( + select( + agent_controls.c.control_id, + agent_controls.c.agent_uuid, + Agent.name, + ) + .select_from(agent_controls) + .join(Agent, Agent.agent_uuid == agent_controls.c.agent_uuid) + .where(agent_controls.c.control_id.in_(control_ids)) + ) + agents_query = union_all(policy_agents_query, direct_agents_query) agents_result = await db.execute(agents_query) for row in agents_result.all(): control_id, agent_uuid, agent_name = row @@ -664,15 +677,15 @@ async def delete_control( control_id: int, force: bool = Query( False, - description="If true, dissociate from all policies before deleting. " - "If false, fail if control is associated with any policy.", + description="If true, dissociate from all policy/agent links before deleting. " + "If false, fail if control is associated with any policy or agent.", ), db: AsyncSession = Depends(get_async_db), ) -> DeleteControlResponse: """ Delete a control by ID. - By default, deletion fails if the control is associated with any policy. + By default, deletion fails if the control is associated with any policy or agent. Use force=true to automatically dissociate and delete. Args: @@ -681,7 +694,7 @@ async def delete_control( db: Database session (injected) Returns: - DeleteControlResponse with success flag and list of dissociated policies + DeleteControlResponse with success flag and dissociation details Raises: HTTPException 404: Control not found @@ -700,48 +713,66 @@ async def delete_control( hint="Verify the control ID is correct and the control has been created.", ) - # Check for associations with policies - assoc_result = await db.execute( - select(policy_controls.c.policy_id).where( - policy_controls.c.control_id == control_id - ) + # Check for associations with policies and direct agent links + policy_assoc_result = await db.execute( + select(policy_controls.c.policy_id).where(policy_controls.c.control_id == control_id) ) - associated_policy_ids = [row[0] for row in assoc_result.all()] + associated_policy_ids = [row[0] for row in policy_assoc_result.all()] - if associated_policy_ids and not force: + agent_assoc_result = await db.execute( + select(agent_controls.c.agent_uuid).where(agent_controls.c.control_id == control_id) + ) + associated_agent_ids = [str(row[0]) for row in agent_assoc_result.all()] + + if (associated_policy_ids or associated_agent_ids) and not force: + errors = [ + ValidationErrorItem( + resource="Policy", + field="controls", + code="control_in_use", + message=f"Control is associated with policy ID {pid}", + value=pid, + ) + for pid in associated_policy_ids + ] + [ + ValidationErrorItem( + resource="Agent", + field="controls", + code="control_in_use", + message=f"Control is directly associated with agent ID {agent_id}", + value=agent_id, + ) + for agent_id in associated_agent_ids + ] raise ConflictError( error_code=ErrorCode.CONTROL_IN_USE, detail=( f"Control '{control.name}' is associated with " - f"{len(associated_policy_ids)} policy/policies" + f"{len(associated_policy_ids)} policy/policies and " + f"{len(associated_agent_ids)} agent(s)" ), resource="Control", resource_id=control.name, hint="Use force=true to dissociate and delete, or remove associations manually first.", - errors=[ - ValidationErrorItem( - resource="Policy", - field="controls", - code="control_in_use", - message=f"Control is associated with policy ID {pid}", - value=pid, - ) - for pid in associated_policy_ids - ], + errors=errors, ) # Remove associations if force=true - dissociated_from: list[int] = [] + dissociated_from_policies: list[int] = [] + dissociated_from_agents: list[str] = [] if associated_policy_ids: - await db.execute( - delete(policy_controls).where( - policy_controls.c.control_id == control_id - ) - ) - dissociated_from = associated_policy_ids + await db.execute(delete(policy_controls).where(policy_controls.c.control_id == control_id)) + dissociated_from_policies = associated_policy_ids + if associated_agent_ids: + await db.execute(delete(agent_controls).where(agent_controls.c.control_id == control_id)) + dissociated_from_agents = associated_agent_ids + if dissociated_from_policies or dissociated_from_agents: _logger.info( - f"Dissociated control '{control.name}' ({control_id}) " - f"from {len(dissociated_from)} policy/policies" + "Dissociated control '%s' (%s) from %s policy/policies and %s agent(s)", + control.name, + control_id, + len(dissociated_from_policies), + len(dissociated_from_agents), ) # Delete the control @@ -761,7 +792,11 @@ async def delete_control( operation="delete", ) - return DeleteControlResponse(success=True, dissociated_from=dissociated_from) + return DeleteControlResponse( + success=True, + dissociated_from_policies=dissociated_from_policies, + dissociated_from_agents=dissociated_from_agents, + ) @router.patch( diff --git a/server/src/agent_control_server/models.py b/server/src/agent_control_server/models.py index 093a3b17..874086bb 100644 --- a/server/src/agent_control_server/models.py +++ b/server/src/agent_control_server/models.py @@ -1,6 +1,6 @@ import datetime as dt import uuid as _uuid -from typing import Any, Optional +from typing import Any from agent_control_models.agent import StepSchema from agent_control_models.base import BaseModel @@ -39,13 +39,31 @@ class AgentData(BaseModel): Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), ) +# Association table for Agent <> Policy many-to-many relationship +agent_policies: Table = Table( + "agent_policies", + Base.metadata, + Column("agent_uuid", ForeignKey("agents.agent_uuid"), primary_key=True, index=True), + Column("policy_id", ForeignKey("policies.id"), primary_key=True, index=True), +) + +# Association table for Agent <> Control many-to-many direct relationship +agent_controls: Table = Table( + "agent_controls", + Base.metadata, + Column("agent_uuid", ForeignKey("agents.agent_uuid"), primary_key=True, index=True), + Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), +) + class Policy(Base): __tablename__ = "policies" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) - agents: Mapped[list["Agent"]] = relationship("Agent", back_populates="policy") + agents: Mapped[list["Agent"]] = relationship( + "Agent", secondary=lambda: agent_policies, back_populates="policies" + ) # Many-to-many: Policy <> Control (direct relationship, no ControlSet layer) controls: Mapped[list["Control"]] = relationship( "Control", secondary=lambda: policy_controls, back_populates="policies" @@ -65,6 +83,10 @@ class Control(Base): policies: Mapped[list["Policy"]] = relationship( "Policy", secondary=lambda: policy_controls, back_populates="controls" ) + # Many-to-many backref: Control <> Agent (direct relationship) + agents: Mapped[list["Agent"]] = relationship( + "Agent", secondary=lambda: agent_controls, back_populates="controls" + ) class EvaluatorConfigDB(Base): @@ -97,10 +119,12 @@ class Agent(Base): data: Mapped[dict[str, Any]] = mapped_column( JSONB, server_default=text("'{}'::jsonb"), nullable=False ) - policy_id: Mapped[int | None] = mapped_column( - ForeignKey("policies.id"), nullable=True, index=True + policies: Mapped[list["Policy"]] = relationship( + "Policy", secondary=lambda: agent_policies, back_populates="agents" + ) + controls: Mapped[list["Control"]] = relationship( + "Control", secondary=lambda: agent_controls, back_populates="agents" ) - policy: Mapped[Optional["Policy"]] = relationship("Policy", back_populates="agents") created_at: Mapped[dt.datetime] = mapped_column( DateTime(), server_default=text("CURRENT_TIMESTAMP"), nullable=False, index=True ) diff --git a/server/src/agent_control_server/services/controls.py b/server/src/agent_control_server/services/controls.py index f7867081..929c103e 100644 --- a/server/src/agent_control_server/services/controls.py +++ b/server/src/agent_control_server/services/controls.py @@ -8,11 +8,11 @@ from agent_control_models.errors import ErrorCode, ValidationErrorItem from agent_control_models.policy import Control as APIControl from pydantic import ValidationError -from sqlalchemy import select +from sqlalchemy import select, union from sqlalchemy.ext.asyncio import AsyncSession from ..errors import APIValidationError -from ..models import Agent, Control, Policy, policy_controls +from ..models import Control, agent_controls, agent_policies, policy_controls _logger = logging.getLogger(__name__) @@ -34,19 +34,30 @@ async def list_controls_for_agent( *, allow_invalid_step_name_regex: bool = False, ) -> list[APIControl]: - """Return API Control models for all configured controls associated with the agent's policy. + """Return API Control models for controls associated with the agent. - Traversal: Agent -> Policy -> Controls (direct relationship). - Uses explicit joins over association table to avoid async relationship loading. + Active controls are the de-duplicated union of: + - controls inherited from all assigned policies + - controls directly associated with the agent Note: Invalid ControlDefinition data triggers an APIValidationError. """ - stmt = ( - select(Control) - .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) + policy_control_ids = ( + select(policy_controls.c.control_id.label("control_id")) + .select_from( + policy_controls.join( + agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id + ) + ) + .where(agent_policies.c.agent_uuid == agent_id) + ) + direct_control_ids = select(agent_controls.c.control_id.label("control_id")).where( + agent_controls.c.agent_uuid == agent_id + ) + control_ids_subquery = union(policy_control_ids, direct_control_ids).subquery() + + stmt = select(Control).join( + control_ids_subquery, Control.id == control_ids_subquery.c.control_id ) result = await db.execute(stmt) diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 600717e3..edcd0003 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -107,6 +107,8 @@ def clean_db(): with engine.begin() as conn: # Delete in dependency order (children before parents) conn.execute(text("DELETE FROM evaluator_configs")) + conn.execute(text("DELETE FROM agent_controls")) + conn.execute(text("DELETE FROM agent_policies")) conn.execute(text("DELETE FROM agents")) conn.execute(text("DELETE FROM policy_controls")) conn.execute(text("DELETE FROM policies")) diff --git a/server/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index 13daec4e..fcca8e22 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -230,7 +230,7 @@ def test_patch_agent_remove_evaluator_in_use_conflict(client: TestClient) -> Non policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign.status_code == 200 # When: attempting to remove evaluator in use @@ -270,7 +270,7 @@ def test_set_agent_policy_incompatible_controls(client: TestClient) -> None: agent_b_id, _ = _init_agent(client) # When: assigning policy to agent B - resp = client.post(f"/api/v1/agents/{agent_b_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") # Then: incompatible controls error assert resp.status_code == 400 @@ -357,7 +357,7 @@ def test_list_agent_controls_corrupted_control_data_returns_422( policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign.status_code == 200 # And: the control data is corrupted in the DB @@ -429,7 +429,7 @@ def test_set_agent_policy_rejects_corrupted_agent_data(client: TestClient) -> No ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -458,7 +458,7 @@ def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -500,7 +500,7 @@ def test_set_agent_policy_rejects_invalid_agent_evaluator_config(client: TestCli ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -514,7 +514,7 @@ def test_get_agent_policy_agent_not_found(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: retrieving policy for a non-existent agent - resp = client.get(f"/api/v1/agents/{missing_agent}/policy") + resp = client.get(f"/api/v1/agents/{missing_agent}/policies") # Then: not found error is returned assert resp.status_code == 404 @@ -526,23 +526,23 @@ def test_delete_agent_policy_agent_not_found(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: deleting policy for a non-existent agent - resp = client.delete(f"/api/v1/agents/{missing_agent}/policy") + resp = client.delete(f"/api/v1/agents/{missing_agent}/policies") # Then: not found error is returned assert resp.status_code == 404 assert resp.json()["error_code"] == "AGENT_NOT_FOUND" -def test_delete_agent_policy_no_policy_assigned_returns_404(client: TestClient) -> None: +def test_delete_agent_policy_no_policy_assigned_is_idempotent(client: TestClient) -> None: # Given: an agent with no policy assigned agent_id, _ = _init_agent(client) # When: deleting policy - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_id}/policies") - # Then: policy not found error is returned - assert resp.status_code == 404 - assert resp.json()["error_code"] == "POLICY_NOT_FOUND" + # Then: deletion is idempotent + assert resp.status_code == 200 + assert resp.json()["success"] is True def test_list_agents_corrupted_data_sets_zero_counts(client: TestClient) -> None: @@ -604,53 +604,16 @@ def test_get_agent_corrupted_metadata_returns_422(client: TestClient) -> None: assert resp.json()["error_code"] == "CORRUPTED_DATA" -def test_get_agent_policy_missing_policy_returns_404(client: TestClient) -> None: - # Given: an agent assigned to a policy that cannot be found +def test_get_agent_policies_returns_empty_when_none_assigned(client: TestClient) -> None: + # Given: an agent with no policy assignments agent_id, _ = _init_agent(client) - policy_id = _create_policy(client) - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") - assert assign.status_code == 200 - from agent_control_server.db import get_async_db - from agent_control_server.main import app - from agent_control_server.models import Agent as AgentModel - from sqlalchemy.orm import Session - from unittest.mock import AsyncMock, MagicMock - from collections.abc import AsyncGenerator - from sqlalchemy.ext.asyncio import AsyncSession - from sqlalchemy import select - - with Session(engine) as session: - agent_row = ( - session.execute( - select(AgentModel).where(AgentModel.agent_uuid == agent_id) - ) - .scalars() - .first() - ) - assert agent_row is not None - - async def mock_db_missing_policy() -> AsyncGenerator[AsyncSession, None]: - mock_session = AsyncMock(spec=AsyncSession) - mock_agent_result = MagicMock() - mock_agent_result.scalars.return_value.first.return_value = agent_row - mock_policy_result = MagicMock() - mock_policy_result.scalars.return_value.first.return_value = None - mock_session.execute = AsyncMock( - side_effect=[mock_agent_result, mock_policy_result] - ) - yield mock_session - - # When: retrieving the agent policy and policy lookup returns None - app.dependency_overrides[get_async_db] = mock_db_missing_policy - try: - resp = client.get(f"/api/v1/agents/{agent_id}/policy") - finally: - app.dependency_overrides.clear() + # When: retrieving associated policies + resp = client.get(f"/api/v1/agents/{agent_id}/policies") - # Then: policy not found error is returned - assert resp.status_code == 404 - assert resp.json()["error_code"] == "POLICY_NOT_FOUND" + # Then: an empty policy list is returned + assert resp.status_code == 200 + assert resp.json()["policy_ids"] == [] def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> None: @@ -664,7 +627,7 @@ def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> Non assert assoc.status_code == 200 # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: assignment succeeds because empty data is ignored during validation assert resp.status_code == 200 @@ -686,7 +649,7 @@ def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClie ) # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: assignment succeeds because evaluator name is missing assert resp.status_code == 200 @@ -704,7 +667,7 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: for control_id in control_ids: assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign.status_code == 200 # When: listing agents @@ -798,7 +761,7 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign.status_code == 200 # When: re-initializing the agent with the same UUID diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 8d63bf9f..1662d3b1 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -382,7 +382,8 @@ def test_delete_control_force_dissociates(client: TestClient) -> None: assert resp2.status_code == 200 body = resp2.json() assert body["success"] is True - assert policy_id in body.get("dissociated_from", []) + assert policy_id in body.get("dissociated_from_policies", []) + assert body.get("dissociated_from_agents") == [] # Then: policy no longer lists the control list_resp = client.get(f"/api/v1/policies/{policy_id}/controls") diff --git a/server/tests/test_error_handling.py b/server/tests/test_error_handling.py index 5a14d8c3..26aca114 100644 --- a/server/tests/test_error_handling.py +++ b/server/tests/test_error_handling.py @@ -79,7 +79,7 @@ def test_delete_agent_policy_rollback_on_failure( assert r2.status_code == 200 policy_id = r2.json()["policy_id"] - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign_resp.status_code == 200 # And: a database session that fails on commit @@ -105,7 +105,7 @@ async def mock_db_for_delete_policy() -> AsyncGenerator[AsyncSession, None]: # When: deleting policy and commit fails app.dependency_overrides[get_async_db] = mock_db_for_delete_policy try: - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_id}/policies") finally: app.dependency_overrides.clear() @@ -320,10 +320,12 @@ async def mock_db_for_delete_control() -> AsyncGenerator[AsyncSession, None]: control_result = MagicMock() control_result.scalars.return_value.first.return_value = existing_control - assoc_result = MagicMock() - assoc_result.all.return_value = [] + policy_assoc_result = MagicMock() + policy_assoc_result.all.return_value = [] + agent_assoc_result = MagicMock() + agent_assoc_result.all.return_value = [] mock_session.execute = AsyncMock( - side_effect=[control_result, assoc_result] + side_effect=[control_result, policy_assoc_result, agent_assoc_result] ) mock_session.delete = AsyncMock() mock_session.rollback = AsyncMock() @@ -415,7 +417,7 @@ async def mock_db_for_policy_assignment() -> AsyncGenerator[AsyncSession, None]: app.dependency_overrides[get_async_db] = mock_db_for_policy_assignment try: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: rollback is called and 500 error is returned assert resp.status_code == 500 diff --git a/server/tests/test_evaluation_e2e.py b/server/tests/test_evaluation_e2e.py index b6e414a8..bea4b961 100644 --- a/server/tests/test_evaluation_e2e.py +++ b/server/tests/test_evaluation_e2e.py @@ -75,7 +75,7 @@ def test_evaluation_empty_policy(client: TestClient): "steps": [] }) - client.post(f"/api/v1/agents/{str(agent_uuid)}/policy/{policy_id}") + client.post(f"/api/v1/agents/{str(agent_uuid)}/policies/{policy_id}") # When: evaluating content for that agent req = EvaluationRequest( @@ -221,8 +221,10 @@ def test_evaluation_deny_precedence(client: TestClient): # 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") - policy_id = resp.json()["policy_id"] + resp = client.get(f"/api/v1/agents/{agent_uuid}/policies") + policy_ids = resp.json()["policy_ids"] + assert len(policy_ids) == 1 + policy_id = policy_ids[0] # Create Deny Control control_deny = { diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index b63e1acb..656f1329 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -286,12 +286,11 @@ def test_set_agent_policy_first_time(client: TestClient) -> None: agent_id = payload["agent"]["agent_id"] # When: assigning policy the first time - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") - # Then: success and no old policy + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + # Then: success assert resp.status_code == 200 body = resp.json() assert body["success"] is True - assert body["old_policy_id"] is None def test_get_agent_policy_after_assignment(client: TestClient) -> None: @@ -300,30 +299,32 @@ def test_get_agent_policy_after_assignment(client: TestClient) -> None: payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # When: retrieving policy - resp = client.get(f"/api/v1/agents/{agent_id}/policy") - # Then: we see the assigned policy id + resp = client.get(f"/api/v1/agents/{agent_id}/policies") + # Then: we see the assigned policy id in the policy_ids list assert resp.status_code == 200 - assert resp.json()["policy_id"] == policy_id + assert resp.json()["policy_ids"] == [policy_id] -def test_reassign_agent_policy_returns_old_id(client: TestClient) -> None: +def test_adding_second_policy_retains_existing_policy(client: TestClient) -> None: # Given: an agent with an existing policy first = _create_policy(client) second = _create_policy(client) payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{first}") + client.post(f"/api/v1/agents/{agent_id}/policies/{first}") - # When: reassigning to another policy - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{second}") - # Then: success and old_policy_id equals the first policy id + # When: adding another policy + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{second}") + # Then: success and both policy associations are retained assert resp.status_code == 200 assert resp.json()["success"] is True - assert resp.json()["old_policy_id"] == first + get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") + assert get_resp.status_code == 200 + assert get_resp.json()["policy_ids"] == [first, second] def test_delete_agent_policy_then_get_404(client: TestClient) -> None: @@ -332,18 +333,19 @@ def test_delete_agent_policy_then_get_404(client: TestClient) -> None: payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # When: removing the policy association - del_resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + del_resp = client.delete(f"/api/v1/agents/{agent_id}/policies") # Then: deletion success assert del_resp.status_code == 200 assert del_resp.json()["success"] is True - # When: fetching policy after deletion - get_resp = client.get(f"/api/v1/agents/{agent_id}/policy") - # Then: not found - assert get_resp.status_code == 404 + # When: fetching policies after deletion + get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") + # Then: empty policy list + assert get_resp.status_code == 200 + assert get_resp.json()["policy_ids"] == [] def test_set_policy_agent_not_found_returns_404(client: TestClient) -> None: @@ -352,7 +354,7 @@ def test_set_policy_agent_not_found_returns_404(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: assigning to missing agent - resp = client.post(f"/api/v1/agents/{missing_agent}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{missing_agent}/policies/{policy_id}") # Then: 404 assert resp.status_code == 404 @@ -365,7 +367,7 @@ def test_set_policy_not_found_returns_404(client: TestClient) -> None: bogus_policy = "999999999" # When: assigning a non-existent policy - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{bogus_policy}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{bogus_policy}") # Then: 404 assert resp.status_code == 404 @@ -405,7 +407,7 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: # Associate control -> policy; assign policy to agent client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # When: listing controls r = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -502,33 +504,33 @@ def test_list_agents_returns_created_agents(client: TestClient) -> None: assert agent1["agent_name"] == "Agent One" assert agent1["step_count"] == 1 # from make_agent_payload assert agent1["evaluator_count"] == 1 - assert agent1["policy_id"] is None + assert agent1["policy_ids"] == [] assert agent2_id in agent_map agent2 = agent_map[agent2_id] assert agent2["agent_name"] == "Agent Two" assert agent2["step_count"] == 2 assert agent2["evaluator_count"] == 0 - assert agent2["policy_id"] is None + assert agent2["policy_ids"] == [] def test_list_agents_with_policy(client: TestClient) -> None: - """Test that list agents shows policy_id when assigned.""" + """Test that list agents shows policy_ids when assigned.""" # Given: an agent with a policy assigned payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] policy_id = _create_policy(client) - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # When: listing agents resp = client.get("/api/v1/agents") - # Then: the agent shows the policy_id + # Then: the agent shows the policy_ids assert resp.status_code == 200 body = resp.json() assert len(body["agents"]) == 1 - assert body["agents"][0]["policy_id"] == policy_id + assert body["agents"][0]["policy_ids"] == [policy_id] def test_list_agents_pagination(client: TestClient) -> None: diff --git a/server/tests/test_new_features.py b/server/tests/test_new_features.py index 9d618ede..9273aa03 100644 --- a/server/tests/test_new_features.py +++ b/server/tests/test_new_features.py @@ -298,7 +298,7 @@ def test_policy_assignment_with_builtin_evaluator(client: TestClient) -> None: ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: assert resp.status_code == 200 @@ -330,7 +330,7 @@ def test_policy_assignment_with_registered_agent_evaluator(client: TestClient) - ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Then: assert resp.status_code == 200 @@ -399,13 +399,13 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No ) # When: Assign to Agent A (should succeed) - resp_a = client.post(f"/api/v1/agents/{agent_a_id}/policy/{policy_id}") + resp_a = client.post(f"/api/v1/agents/{agent_a_id}/policies/{policy_id}") # Then: assert resp_a.status_code == 200 # When: Assign same policy to Agent B (should fail) - resp_b = client.post(f"/api/v1/agents/{agent_b_id}/policy/{policy_id}") + resp_b = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") # Then: (RFC 7807 format) assert resp_b.status_code == 400 @@ -561,7 +561,7 @@ def test_patch_agent_remove_evaluator_blocked_by_control(client: TestClient) -> ) # And: Policy assigned to agent - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign_resp.status_code == 200 # When: Trying to remove the evaluator diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index b5789421..136ac9f9 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -69,7 +69,7 @@ def test_agent_gets_controls_from_policy(client: TestClient) -> None: assert resp.status_code == 200 # Assign policy to agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert resp.status_code == 200 # When: Get agent's controls @@ -96,7 +96,7 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) client.post(f"/api/v1/policies/{policy_id}/controls/{control_1_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Verify initial state: 2 controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -123,8 +123,8 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) assert control_ids == {control_1_id, control_2_id, control_3_id, control_4_id, control_5_id} -def test_switching_agent_policy_changes_controls(client: TestClient) -> None: - """Switching agent's policy should completely change its controls.""" +def test_adding_second_agent_policy_unions_controls(client: TestClient) -> None: + """Adding another policy should union controls from both policies.""" # Given: Two policies with different controls agent_id, _ = _create_agent(client) @@ -143,21 +143,26 @@ def test_switching_agent_policy_changes_controls(client: TestClient) -> None: client.post(f"/api/v1/policies/{policy_b_id}/controls/{control_4_id}") # Assign policy A to agent - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_a_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") resp = client.get(f"/api/v1/agents/{agent_id}/controls") controls_a = resp.json()["controls"] assert len(controls_a) == 2 assert {r["id"] for r in controls_a} == {control_1_id, control_2_id} - # When: Switch to policy B - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_b_id}") + # When: Add policy B + resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") assert resp.status_code == 200 - # Then: Agent's controls change completely + # Then: Agent sees controls from both policies resp = client.get(f"/api/v1/agents/{agent_id}/controls") controls_b = resp.json()["controls"] - assert len(controls_b) == 2 - assert {r["id"] for r in controls_b} == {control_3_id, control_4_id} + assert len(controls_b) == 4 + assert {r["id"] for r in controls_b} == { + control_1_id, + control_2_id, + control_3_id, + control_4_id, + } def test_removing_agent_policy_clears_controls(client: TestClient) -> None: @@ -168,14 +173,14 @@ def test_removing_agent_policy_clears_controls(client: TestClient) -> None: control_id = _create_control(client, "control-1", {"id": 1}) client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Verify agent has controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") assert len(resp.json()["controls"]) > 0 # When: Remove policy from agent - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_id}/policies") assert resp.status_code == 200 # Then: Agent returns empty controls list @@ -199,7 +204,7 @@ def test_removing_control_from_policy_removes_from_agent(client: TestClient) -> client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_3_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_4_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") # Verify initial state: 4 controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -233,8 +238,8 @@ def test_multiple_agents_same_policy(client: TestClient) -> None: client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") # Assign same policy to both agents - client.post(f"/api/v1/agents/{agent_1_id}/policy/{policy_id}") - client.post(f"/api/v1/agents/{agent_2_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_1_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_2_id}/policies/{policy_id}") # Verify both see same controls initially resp_1 = client.get(f"/api/v1/agents/{agent_1_id}/controls") @@ -284,8 +289,8 @@ def test_control_shared_between_policies(client: TestClient) -> None: agent_a_id, _ = _create_agent(client, "agent-a") agent_b_id, _ = _create_agent(client, "agent-b") - 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}") + client.post(f"/api/v1/agents/{agent_a_id}/policies/{policy_a_id}") + client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_b_id}") resp_a = client.get(f"/api/v1/agents/{agent_a_id}/controls") resp_b = client.get(f"/api/v1/agents/{agent_b_id}/controls") diff --git a/server/tests/test_services_controls.py b/server/tests/test_services_controls.py index e4d26670..b560fcb2 100644 --- a/server/tests/test_services_controls.py +++ b/server/tests/test_services_controls.py @@ -7,7 +7,14 @@ from agent_control_models.errors import ErrorCode from agent_control_server.errors import APIValidationError -from agent_control_server.models import Agent, Control, Policy, policy_controls +from agent_control_server.models import ( + Agent, + Control, + Policy, + agent_controls, + agent_policies, + policy_controls, +) from agent_control_server.services.controls import list_controls_for_agent, list_controls_for_policy from .utils import VALID_CONTROL_PAYLOAD @@ -42,20 +49,28 @@ async def test_list_controls_for_policy_returns_controls(async_db) -> None: @pytest.mark.asyncio async def test_list_controls_for_agent_returns_controls(async_db) -> None: - # Given: an agent assigned to a policy with one control + # Given: an agent associated with one policy control and one direct control policy = Policy(name=f"policy-{uuid.uuid4()}") - control = Control(name=f"control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) + policy_control = Control(name=f"policy-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) + direct_control = Control(name=f"direct-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) agent = Agent( agent_uuid=uuid.uuid4(), name=f"agent-{uuid.uuid4()}", data={}, - policy=policy, ) - async_db.add_all([policy, control, agent]) + async_db.add_all([policy, policy_control, direct_control, agent]) await async_db.flush() await async_db.execute( - insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) + insert(agent_policies).values({"agent_uuid": agent.agent_uuid, "policy_id": policy.id}) + ) + await async_db.execute( + insert(policy_controls).values({"policy_id": policy.id, "control_id": policy_control.id}) + ) + await async_db.execute( + insert(agent_controls).values( + {"agent_uuid": agent.agent_uuid, "control_id": direct_control.id} + ) ) await async_db.commit() @@ -63,25 +78,27 @@ async def test_list_controls_for_agent_returns_controls(async_db) -> None: controls = await list_controls_for_agent(agent.agent_uuid, async_db) # Then: the API control is returned with expected fields - assert len(controls) == 1 - assert controls[0].name == control.name - assert controls[0].control.evaluator.name == VALID_CONTROL_PAYLOAD["evaluator"]["name"] + assert len(controls) == 2 + names = {control.name for control in controls} + assert names == {policy_control.name, direct_control.name} @pytest.mark.asyncio async def test_list_controls_for_agent_corrupted_data_raises(async_db) -> None: - # Given: an agent assigned to a policy with corrupted control data + # Given: an agent associated with a policy containing corrupted control data 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, ) async_db.add_all([policy, control, agent]) await async_db.flush() + await async_db.execute( + insert(agent_policies).values({"agent_uuid": agent.agent_uuid, "policy_id": policy.id}) + ) await async_db.execute( insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) ) diff --git a/server/tests/utils.py b/server/tests/utils.py index 73efa678..166a8c33 100644 --- a/server/tests/utils.py +++ b/server/tests/utils.py @@ -65,7 +65,7 @@ def create_and_assign_policy( 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/{str(agent_uuid)}/policies/{policy_id}") assert resp.status_code == 200 return agent_uuid, control_name diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index c72911a9..a071fafe 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -65,18 +65,30 @@ export const api = { apiClient.GET('/api/v1/agents/{agent_id}/controls', { params: { path: { agent_id: agentId } }, }), - setPolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => - apiClient.POST('/api/v1/agents/{agent_id}/policy/{policy_id}', { + addPolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => + apiClient.POST('/api/v1/agents/{agent_id}/policies/{policy_id}', { params: { path: { agent_id: agentId, policy_id: policyId } }, }), - getPolicy: (agentId: GetAgentPathParams['agent_id']) => - apiClient.GET('/api/v1/agents/{agent_id}/policy', { + removePolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => + apiClient.DELETE('/api/v1/agents/{agent_id}/policies/{policy_id}', { + params: { path: { agent_id: agentId, policy_id: policyId } }, + }), + getPolicies: (agentId: GetAgentPathParams['agent_id']) => + apiClient.GET('/api/v1/agents/{agent_id}/policies', { params: { path: { agent_id: agentId } }, }), - deletePolicy: (agentId: GetAgentPathParams['agent_id']) => - apiClient.DELETE('/api/v1/agents/{agent_id}/policy', { + clearPolicies: (agentId: GetAgentPathParams['agent_id']) => + apiClient.DELETE('/api/v1/agents/{agent_id}/policies', { params: { path: { agent_id: agentId } }, }), + addControl: (agentId: GetAgentPathParams['agent_id'], controlId: number) => + apiClient.POST('/api/v1/agents/{agent_id}/controls/{control_id}', { + params: { path: { agent_id: agentId, control_id: controlId } }, + }), + removeControl: (agentId: GetAgentPathParams['agent_id'], controlId: number) => + apiClient.DELETE('/api/v1/agents/{agent_id}/controls/{control_id}', { + params: { path: { agent_id: agentId, control_id: controlId } }, + }), }, evaluators: { list: () => apiClient.GET('/api/v1/evaluators'), diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index c1e36591..3a57b1b2 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -4,3933 +4,4065 @@ */ export interface paths { - '/api/v1/agents': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all agents - * @description List all registered agents with cursor-based pagination. - * - * Returns a summary of each agent including ID, name, policy assignment, - * and counts of registered steps and evaluators. - * - * Args: - * cursor: Optional cursor for pagination (UUID of last agent from previous page) - * limit: Pagination limit (default 20, max 100) - * name: Optional name filter (case-insensitive partial match) - * db: Database session (injected) - * - * Returns: - * ListAgentsResponse with agent summaries and pagination info - */ - get: operations['list_agents_api_v1_agents_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/initAgent': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Initialize or update an agent - * @description Register a new agent or update an existing agent's steps and metadata. - * - * 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 - * - * Step versioning: When step schemas change (input_schema or output_schema), - * a new version is created automatically. - * - * Args: - * request: Agent metadata and step schemas - * db: Database session (injected) - * - * 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; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent details - * @description 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 - * db: Database session (injected) - * - * Returns: - * GetAgentResponse with agent metadata and step list - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 422: Agent data is corrupted - */ - get: operations['get_agent_api_v1_agents__agent_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify agent (remove steps/evaluators) - * @description Remove steps and/or evaluators from an agent. - * - * This is the complement to initAgent which only adds items. - * Removals are idempotent - attempting to remove non-existent items is not an error. - * - * Args: - * agent_id: UUID of the agent - * request: Lists of step/evaluator identifiers to remove - * db: Database session (injected) - * - * Returns: - * PatchAgentResponse with lists of actually removed items - * - * Raises: - * 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; - trace?: never; - }; - '/api/v1/agents/{agent_id}/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's active controls - * @description List all protection controls active for an agent. - * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. - * - * Args: - * agent_id: UUID of the agent - * db: Database session (injected) - * - * Returns: - * AgentControlsResponse with list of controls (empty if no policy) - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_controls_api_v1_agents__agent_id__controls_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_id}/evaluators': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's registered evaluator schemas - * @description List all evaluator schemas registered with an agent. - * - * Evaluator schemas are registered via initAgent and used for: - * - Config validation when creating Controls - * - UI to display available config options - * - * Args: - * agent_id: UUID of the agent - * cursor: Optional cursor for pagination (name of last evaluator from previous page) - * limit: Pagination limit (default 20, max 100) - * db: Database session (injected) - * - * Returns: - * ListEvaluatorsResponse with evaluator schemas and pagination - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_evaluators_api_v1_agents__agent_id__evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_id}/evaluators/{evaluator_name}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get specific evaluator schema - * @description Get a specific evaluator schema registered with an agent. - * - * Args: - * agent_id: UUID of the agent - * evaluator_name: Name of the evaluator - * db: Database session (injected) - * - * Returns: - * EvaluatorSchemaItem with schema details - * - * Raises: - * HTTPException 404: Agent or evaluator not found - */ - get: operations['get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies': { - 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. - * - * 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/{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; - }; - '/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/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all controls - * @description List all controls with optional filtering and cursor-based pagination. - * - * Controls are returned ordered by ID descending (newest first). - * - * Args: - * cursor: ID of the last control from the previous page (for pagination) - * limit: Maximum number of controls to return (default 20, max 100) - * name: Optional filter by name (partial, case-insensitive match) - * enabled: Optional filter by enabled status - * step_type: Optional filter by step type (built-ins: 'tool', 'llm') - * stage: Optional filter by stage ('pre' or 'post') - * execution: Optional filter by execution ('server' or 'sdk') - * tag: Optional filter by tag - * db: Database session (injected) - * - * Returns: - * ListControlsResponse with control summaries and pagination info - * - * Example: - * GET /controls?limit=10&enabled=true&step_type=tool - */ - get: operations['list_controls_api_v1_controls_get']; - /** - * Create a new control - * @description Create a new control with a unique name and empty data. - * - * Controls define protection logic and can be added to policies. - * Use the PUT /{control_id}/data endpoint to set control configuration. - * - * Args: - * request: Control creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreateControlResponse with the new control's ID - * - * Raises: - * HTTPException 409: Control with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations['create_control_api_v1_controls_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control details - * @description Retrieve a control by ID including its name and configuration data. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlResponse with control id, name, and data - * - * Raises: - * HTTPException 404: Control not found - */ - get: operations['get_control_api_v1_controls__control_id__get']; - put?: never; - post?: never; - /** - * Delete a control - * @description Delete a control by ID. - * - * By default, deletion fails if the control is associated with any policy. - * Use force=true to automatically dissociate and delete. - * - * Args: - * control_id: ID of the control to delete - * force: If true, remove associations before deleting - * db: Database session (injected) - * - * Returns: - * DeleteControlResponse with success flag and list of dissociated policies - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: Control is in use (and force=false) - * HTTPException 500: Database error during deletion - */ - delete: operations['delete_control_api_v1_controls__control_id__delete']; - options?: never; - head?: never; - /** - * Update control metadata - * @description Update control metadata (name and/or enabled status). - * - * This endpoint allows partial updates: - * - To rename: provide 'name' field - * - To enable/disable: provide 'enabled' field (updates the control's data) - * - * Args: - * control_id: ID of the control to update - * request: Fields to update (name, enabled) - * db: Database session (injected) - * - * Returns: - * PatchControlResponse with current control state - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: New name conflicts with existing control - * HTTPException 422: Cannot update enabled status (control has no data configured) - * HTTPException 500: Database error during update - */ - patch: operations['patch_control_api_v1_controls__control_id__patch']; - trace?: never; - }; - '/api/v1/controls/{control_id}/data': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control configuration data - * @description Retrieve the configuration data for a control. - * - * Control data is a JSONB field that must follow the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlDataResponse with validated ControlDefinition - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 422: Control data is corrupted - */ - get: operations['get_control_data_api_v1_controls__control_id__data_get']; - /** - * Update control configuration data - * @description Update the configuration data for a control. - * - * This replaces the entire data payload. The data is validated against - * the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * request: New control data (replaces existing) - * db: Database session (injected) - * - * Returns: - * SetControlDataResponse with success flag - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 500: Database error during update - */ - put: operations['set_control_data_api_v1_controls__control_id__data_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluator-configs': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List evaluator configs */ - get: operations['list_evaluator_configs_api_v1_evaluator_configs_get']; - put?: never; - /** Create evaluator config */ - post: operations['create_evaluator_config_api_v1_evaluator_configs_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluator-configs/{config_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get evaluator config */ - get: operations['get_evaluator_config_api_v1_evaluator_configs__config_id__get']; - /** Update evaluator config */ - put: operations['update_evaluator_config_api_v1_evaluator_configs__config_id__put']; - post?: never; - /** Delete evaluator config */ - delete: operations['delete_evaluator_config_api_v1_evaluator_configs__config_id__delete']; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List available evaluators - * @description List all available evaluators. - * - * Returns metadata and JSON Schema for each built-in evaluator. - * - * Built-in evaluators: - * - **regex**: Regular expression pattern matching - * - **list**: List-based value matching with flexible logic - * - **json**: JSON validation with schema, types, constraints - * - **sql**: SQL query validation - * - * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. - */ - get: operations['get_evaluators_api_v1_evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ingest Events - * @description Ingest batched control execution events. - * - * Events are stored directly to the database with ~5-20ms latency. - * - * Args: - * request: Batch of events to ingest - * ingestor: Event ingestor (injected) - * - * Returns: - * BatchEventsResponse with counts of received/processed/dropped - */ - post: operations['ingest_events_api_v1_observability_events_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events/query': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Query Events - * @description Query raw control execution events. - * - * Supports filtering by: - * - 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 - * - control_ids: Filter by controls - * - actions: Filter by actions (allow, deny, warn, log) - * - matched: Filter by matched status - * - check_stages: Filter by check stage (pre, post) - * - applies_to: Filter by call type (llm_call, tool_call) - * - start_time/end_time: Filter by time range - * - * Results are paginated with limit/offset. - * - * Args: - * request: Query parameters - * store: Event store (injected) - * - * Returns: - * EventQueryResponse with matching events and pagination info - */ - post: operations['query_events_api_v1_observability_events_query_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Stats - * @description Get agent-level aggregated statistics. - * - * Returns totals across all controls plus per-control breakdown. - * Use /stats/controls/{control_id} for single control stats. - * - * Args: - * agent_uuid: 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) - * - * Returns: - * StatsResponse with agent-level totals and per-control breakdown - */ - get: operations['get_stats_api_v1_observability_stats_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Control Stats - * @description Get statistics for a single control. - * - * Returns stats for the specified control with optional time-series. - * - * Args: - * control_id: Control ID to get stats for - * agent_uuid: 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) - * - * Returns: - * ControlStatsResponse with control stats and optional timeseries - */ - get: operations['get_control_stats_api_v1_observability_stats_controls__control_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Status - * @description Get observability system status. - * - * Returns basic health information. - */ - get: operations['get_status_api_v1_observability_status_get']; - put?: never; - post?: never; - delete?: never; - 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; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; + "/api/v1/agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all agents + * @description List all registered agents with cursor-based pagination. + * + * Returns a summary of each agent including ID, name, policy associations, + * and counts of registered steps and evaluators. + * + * Args: + * cursor: Optional cursor for pagination (UUID of last agent from previous page) + * limit: Pagination limit (default 20, max 100) + * name: Optional name filter (case-insensitive partial match) + * db: Database session (injected) + * + * Returns: + * ListAgentsResponse with agent summaries and pagination info + */ + get: operations["list_agents_api_v1_agents_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/initAgent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Initialize or update an agent + * @description Register a new agent or update an existing agent's steps and metadata. + * + * 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 UUID exists with a different name, returns 409 Conflict (no renames) + * + * Step versioning: When step schemas change (input_schema or output_schema), + * a new version is created automatically. + * + * Args: + * request: Agent metadata and step schemas + * db: Database session (injected) + * + * 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; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent details + * @description 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 + * db: Database session (injected) + * + * Returns: + * GetAgentResponse with agent metadata and step list + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 422: Agent data is corrupted + */ + get: operations["get_agent_api_v1_agents__agent_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify agent (remove steps/evaluators) + * @description Remove steps and/or evaluators from an agent. + * + * This is the complement to initAgent which only adds items. + * Removals are idempotent - attempting to remove non-existent items is not an error. + * + * Args: + * agent_id: UUID of the agent + * request: Lists of step/evaluator identifiers to remove + * db: Database session (injected) + * + * Returns: + * PatchAgentResponse with lists of actually removed items + * + * Raises: + * 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}/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's active controls + * @description List all protection controls active for an agent. + * + * Controls include the union of policy-derived and directly associated controls. + * + * Args: + * agent_id: UUID of the agent + * db: Database session (injected) + * + * Returns: + * AgentControlsResponse with list of active controls + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_controls_api_v1_agents__agent_id__controls_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate control directly with agent + * @description Associate a control directly with an agent (idempotent). + */ + post: operations["add_agent_control_api_v1_agents__agent_id__controls__control_id__post"]; + /** + * Remove direct control association from agent + * @description Remove a direct control association from an agent (idempotent). + */ + delete: operations["remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's registered evaluator schemas + * @description List all evaluator schemas registered with an agent. + * + * Evaluator schemas are registered via initAgent and used for: + * - Config validation when creating Controls + * - UI to display available config options + * + * Args: + * agent_id: UUID of the agent + * cursor: Optional cursor for pagination (name of last evaluator from previous page) + * limit: Pagination limit (default 20, max 100) + * db: Database session (injected) + * + * Returns: + * ListEvaluatorsResponse with evaluator schemas and pagination + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_evaluators_api_v1_agents__agent_id__evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}/evaluators/{evaluator_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get specific evaluator schema + * @description Get a specific evaluator schema registered with an agent. + * + * Args: + * agent_id: UUID of the agent + * evaluator_name: Name of the evaluator + * db: Database session (injected) + * + * Returns: + * EvaluatorSchemaItem with schema details + * + * Raises: + * HTTPException 404: Agent or evaluator not found + */ + get: operations["get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}/policies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policies associated with agent + * @description List policy IDs associated with an agent. + */ + get: operations["get_agent_policies_api_v1_agents__agent_id__policies_get"]; + put?: never; + post?: never; + /** + * Remove all policy associations from agent + * @description Remove all policy associations from an agent. + */ + delete: operations["remove_all_agent_policies_api_v1_agents__agent_id__policies_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_id}/policies/{policy_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate policy with agent + * @description Associate a policy with an agent (idempotent). + */ + post: operations["add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post"]; + /** + * Remove policy association from agent + * @description Remove a policy association from an agent (idempotent). + */ + delete: operations["remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all controls + * @description List all controls with optional filtering and cursor-based pagination. + * + * Controls are returned ordered by ID descending (newest first). + * + * Args: + * cursor: ID of the last control from the previous page (for pagination) + * limit: Maximum number of controls to return (default 20, max 100) + * name: Optional filter by name (partial, case-insensitive match) + * enabled: Optional filter by enabled status + * step_type: Optional filter by step type (built-ins: 'tool', 'llm') + * stage: Optional filter by stage ('pre' or 'post') + * execution: Optional filter by execution ('server' or 'sdk') + * tag: Optional filter by tag + * db: Database session (injected) + * + * Returns: + * ListControlsResponse with control summaries and pagination info + * + * Example: + * GET /controls?limit=10&enabled=true&step_type=tool + */ + get: operations["list_controls_api_v1_controls_get"]; + /** + * Create a new control + * @description Create a new control with a unique name and empty data. + * + * Controls define protection logic and can be added to policies. + * Use the PUT /{control_id}/data endpoint to set control configuration. + * + * Args: + * request: Control creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreateControlResponse with the new control's ID + * + * Raises: + * HTTPException 409: Control with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations["create_control_api_v1_controls_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control details + * @description Retrieve a control by ID including its name and configuration data. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlResponse with control id, name, and data + * + * Raises: + * HTTPException 404: Control not found + */ + get: operations["get_control_api_v1_controls__control_id__get"]; + put?: never; + post?: never; + /** + * Delete a control + * @description Delete a control by ID. + * + * By default, deletion fails if the control is associated with any policy or agent. + * Use force=true to automatically dissociate and delete. + * + * Args: + * control_id: ID of the control to delete + * force: If true, remove associations before deleting + * db: Database session (injected) + * + * Returns: + * DeleteControlResponse with success flag and dissociation details + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: Control is in use (and force=false) + * HTTPException 500: Database error during deletion + */ + delete: operations["delete_control_api_v1_controls__control_id__delete"]; + options?: never; + head?: never; + /** + * Update control metadata + * @description Update control metadata (name and/or enabled status). + * + * This endpoint allows partial updates: + * - To rename: provide 'name' field + * - To enable/disable: provide 'enabled' field (updates the control's data) + * + * Args: + * control_id: ID of the control to update + * request: Fields to update (name, enabled) + * db: Database session (injected) + * + * Returns: + * PatchControlResponse with current control state + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: New name conflicts with existing control + * HTTPException 422: Cannot update enabled status (control has no data configured) + * HTTPException 500: Database error during update + */ + patch: operations["patch_control_api_v1_controls__control_id__patch"]; + trace?: never; + }; + "/api/v1/controls/{control_id}/data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control configuration data + * @description Retrieve the configuration data for a control. + * + * Control data is a JSONB field that must follow the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlDataResponse with validated ControlDefinition + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 422: Control data is corrupted + */ + get: operations["get_control_data_api_v1_controls__control_id__data_get"]; + /** + * Update control configuration data + * @description Update the configuration data for a control. + * + * This replaces the entire data payload. The data is validated against + * the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * request: New control data (replaces existing) + * db: Database session (injected) + * + * Returns: + * SetControlDataResponse with success flag + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 500: Database error during update + */ + put: operations["set_control_data_api_v1_controls__control_id__data_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** List evaluator configs */ + get: operations["list_evaluator_configs_api_v1_evaluator_configs_get"]; + put?: never; + /** Create evaluator config */ + post: operations["create_evaluator_config_api_v1_evaluator_configs_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluator-configs/{config_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get evaluator config */ + get: operations["get_evaluator_config_api_v1_evaluator_configs__config_id__get"]; + /** Update evaluator config */ + put: operations["update_evaluator_config_api_v1_evaluator_configs__config_id__put"]; + post?: never; + /** Delete evaluator config */ + delete: operations["delete_evaluator_config_api_v1_evaluator_configs__config_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available evaluators + * @description List all available evaluators. + * + * Returns metadata and JSON Schema for each built-in evaluator. + * + * Built-in evaluators: + * - **regex**: Regular expression pattern matching + * - **list**: List-based value matching with flexible logic + * - **json**: JSON validation with schema, types, constraints + * - **sql**: SQL query validation + * + * Custom evaluators are registered per-agent via initAgent. + * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + */ + get: operations["get_evaluators_api_v1_evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ingest Events + * @description Ingest batched control execution events. + * + * Events are stored directly to the database with ~5-20ms latency. + * + * Args: + * request: Batch of events to ingest + * ingestor: Event ingestor (injected) + * + * Returns: + * BatchEventsResponse with counts of received/processed/dropped + */ + post: operations["ingest_events_api_v1_observability_events_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events/query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Query Events + * @description Query raw control execution events. + * + * Supports filtering by: + * - 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 + * - control_ids: Filter by controls + * - actions: Filter by actions (allow, deny, warn, log) + * - matched: Filter by matched status + * - check_stages: Filter by check stage (pre, post) + * - applies_to: Filter by call type (llm_call, tool_call) + * - start_time/end_time: Filter by time range + * + * Results are paginated with limit/offset. + * + * Args: + * request: Query parameters + * store: Event store (injected) + * + * Returns: + * EventQueryResponse with matching events and pagination info + */ + post: operations["query_events_api_v1_observability_events_query_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Stats + * @description Get agent-level aggregated statistics. + * + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. + * + * Args: + * agent_uuid: 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) + * + * Returns: + * StatsResponse with agent-level totals and per-control breakdown + */ + get: operations["get_stats_api_v1_observability_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID to get stats for + * agent_uuid: 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) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Status + * @description Get observability system status. + * + * Returns basic health information. + */ + get: operations["get_status_api_v1_observability_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/policies": { + 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. + * + * 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; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - /** - * Agent - * @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. - * @example { - * "agent_description": "Handles customer inquiries and support tickets", - * "agent_id": "550e8400-e29b-41d4-a716-446655440000", - * "agent_metadata": { - * "environment": "production", - * "team": "support" - * }, - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * } - */ - 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_name: string; - /** - * 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_created_at?: string | null; - /** - * Agent Updated At - * @description ISO 8601 timestamp when agent was last updated - */ - agent_updated_at?: string | null; - /** - * Agent Version - * @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: { - /** - * Controls - * @description List of controls associated with the agent via its policy - */ - controls: components['schemas']['Control'][]; - }; - /** - * AgentRef - * @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 - */ - agent_name: string; - }; - /** - * AgentSummary - * @description Summary of an agent for list responses. - */ - AgentSummary: { - /** - * Agent Id - * @description UUID of the agent - */ - agent_id: string; - /** - * Agent Name - * @description Human-readable name 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 - * @default 0 - */ - evaluator_count: number; - /** - * Active Controls Count - * @description Number of active controls from agent's policy - * @default 0 - */ - active_controls_count: number; - }; - /** AssocResponse */ - AssocResponse: { - /** - * Success - * @description Whether the association change succeeded - */ - success: boolean; - }; - /** - * BatchEventsRequest - * @description Request model for batch event ingestion. - * - * SDKs batch events and send them to the server periodically. - * This reduces HTTP overhead significantly (100x reduction). - * - * Attributes: - * events: List of control execution events to ingest - * @example { - * "events": [ - * { - * "action": "deny", - * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_id": 123, - * "control_name": "sql-injection-check", - * "matched": true, - * "span_id": "00f067aa0ba902b7", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * ] - * } - */ - BatchEventsRequest: { - /** - * Events - * @description List of events to ingest - */ - events: components['schemas']['ControlExecutionEvent'][]; - }; - /** - * BatchEventsResponse - * @description Response model for batch event ingestion. - * - * Attributes: - * received: Number of events received - * enqueued: Number of events successfully enqueued - * dropped: Number of events dropped (queue full) - * status: Overall status ('queued', 'partial', 'failed') - */ - BatchEventsResponse: { - /** - * Received - * @description Number of events received - */ - received: number; - /** - * Enqueued - * @description Number of events enqueued - */ - enqueued: number; - /** - * Dropped - * @description Number of events dropped - */ - dropped: number; - /** - * Status - * @description Overall ingestion status - * @enum {string} - */ - status: 'queued' | 'partial' | 'failed'; - }; - /** - * Control - * @description A control with identity and configuration. - * - * Note: Only fully-configured controls (with valid ControlDefinition) - * are returned from API endpoints. Unconfigured controls are filtered out. - */ - Control: { - /** Id */ - id: number; - /** Name */ - name: string; - control: components['schemas']['ControlDefinition']; - }; - /** - * ControlAction - * @description What to do when control matches. - */ - ControlAction: { - /** - * Decision - * @description Action to take when control is triggered - * @enum {string} - */ - decision: 'allow' | 'deny' | 'warn' | 'log'; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - ControlDefinition: { - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: 'server' | 'sdk'; - /** @description Which steps and stages this control applies to */ - 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 - */ - tags?: string[]; - }; - /** - * ControlExecutionEvent - * @description Represents a single control execution event. - * - * This is the core observability data model, capturing: - * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) - * - Context: agent, control, check stage, applies to - * - Result: action taken, whether matched, confidence score - * - Timing: when it happened, how long it took - * - Optional details: evaluator name, selector path, errors, metadata - * - * Attributes: - * 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) - * 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) - * matched: Whether the control evaluator matched - * confidence: Confidence score from the evaluator (0.0-1.0) - * timestamp: When the control was executed (UTC) - * execution_duration_ms: How long the control evaluation took - * evaluator_name: Name of the evaluator used - * selector_path: The selector path used to extract data - * error_message: Error message if evaluation failed - * metadata: Additional metadata for extensibility - * @example { - * "action": "deny", - * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", - * "control_id": 123, - * "control_name": "sql-injection-check", - * "evaluator_name": "regex", - * "execution_duration_ms": 15.3, - * "matched": true, - * "selector_path": "input", - * "span_id": "00f067aa0ba902b7", - * "timestamp": "2025-01-09T10:30:00Z", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - */ - ControlExecutionEvent: { - /** - * Control Execution Id - * @description Unique ID for this control execution - */ - control_execution_id?: string; - /** - * Trace Id - * @description Trace ID for distributed tracing (SDK generates OTEL-compatible 32-char hex) - */ - trace_id: string; - /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) - */ - span_id: string; - /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent - */ - agent_uuid: string; - /** - * Agent Name - * @description Name of the agent (denormalized) - */ - agent_name: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control (denormalized) - */ - control_name: string; - /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} - */ - check_stage: 'pre' | 'post'; - /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} - */ - applies_to: 'llm_call' | 'tool_call'; - /** - * Action - * @description Action taken by the control - * @enum {string} - */ - action: 'allow' | 'deny' | 'warn' | 'log'; - /** - * 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 - */ - evaluator_name?: string | null; - /** - * Selector Path - * @description Selector path used to extract data - */ - selector_path?: string | null; - /** - * Error Message - * @description Error message if evaluation failed - */ - error_message?: string | null; - /** - * Metadata - * @description Additional metadata - */ - metadata?: { - [key: string]: unknown; - }; - }; - /** - * ControlMatch - * @description Represents a control evaluation result (match, non-match, or error). - */ - ControlMatch: { - /** - * Control Execution Id - * @description Unique ID for this control execution (generated by engine) - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @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']; - }; - /** - * ControlScope - * @description Defines when a control applies to a Step. - * @example { - * "stages": [ - * "pre" - * ], - * "step_types": [ - * "tool" - * ] - * } - * @example { - * "step_names": [ - * "search_db", - * "fetch_user" - * ] - * } - * @example { - * "step_name_regex": "^db_.*" - * } - * @example { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * } - */ - 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 - */ - step_names?: string[] | 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 - */ - stages?: ('pre' | 'post')[] | null; - }; - /** - * ControlSelector - * @description Selects data from a Step payload. - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - * @example { - * "path": "output" - * } - * @example { - * "path": "context.user_id" - * } - * @example { - * "path": "input" - * } - * @example { - * "path": "*" - * } - * @example { - * "path": "name" - * } - * @example { - * "path": "output" - * } - */ - ControlSelector: { - /** - * Path - * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' - * @default * - */ - path: string | null; - }; - /** - * ControlStats - * @description Aggregated statistics for a single control. - * - * Attributes: - * control_id: Database ID of the control - * control_name: Name of the control - * execution_count: Total number of executions - * match_count: Number of times the control matched - * non_match_count: Number of times the control did not match - * allow_count: Number of allow actions - * deny_count: Number of deny actions - * warn_count: Number of warn actions - * log_count: Number of log actions - * error_count: Number of errors during evaluation - * avg_confidence: Average confidence score - * avg_duration_ms: Average execution duration in milliseconds - */ - ControlStats: { - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - control_name: string; - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Match Count - * @description Total matches - */ - match_count: number; - /** - * Non Match Count - * @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 - * @description Response model for control-level statistics. - * - * Contains stats for a single control (with optional timeseries). - * - * Attributes: - * agent_uuid: Agent UUID - * time_range: Time range used - * control_id: Control ID - * control_name: Control name - * stats: Control statistics (includes timeseries when requested) - */ - ControlStatsResponse: { - /** - * Agent Uuid - * Format: uuid - * @description Agent UUID - */ - agent_uuid: string; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - control_name: string; - /** @description Control statistics */ - stats: components['schemas']['StatsTotals']; - }; - /** - * 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 - */ - description?: string | null; - /** - * Enabled - * @description Whether control is enabled - * @default true - */ - enabled: boolean; - /** - * Execution - * @description 'server' or 'sdk' - */ - execution?: string | null; - /** - * Step Types - * @description Step types in scope - */ - step_types?: string[] | null; - /** - * Stages - * @description Evaluation stages in scope - */ - stages?: string[] | null; - /** - * Tags - * @description Control tags - */ - tags?: string[]; - /** @description Agent using this control */ - used_by_agent?: components['schemas']['AgentRef'] | null; - }; - /** CreateControlRequest */ - CreateControlRequest: { - /** - * Name - * @description Unique control name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreateControlResponse */ - CreateControlResponse: { - /** - * Control Id - * @description Identifier of the created control - */ - control_id: number; - }; - /** - * CreateEvaluatorConfigRequest - * @description Request to create an evaluator config template. - */ - CreateEvaluatorConfigRequest: { - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - }; - /** CreatePolicyRequest */ - CreatePolicyRequest: { - /** - * Name - * @description Unique policy name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyResponse */ - CreatePolicyResponse: { - /** - * Policy Id - * @description Identifier of the created policy - */ - policy_id: number; - }; - /** - * DeleteControlResponse - * @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[]; - }; - /** - * DeleteEvaluatorConfigResponse - * @description Response for deleting an evaluator config. - */ - DeleteEvaluatorConfigResponse: { - /** - * Success - * @description Whether the evaluator config was deleted - */ - success: boolean; - }; - /** DeletePolicyResponse */ - DeletePolicyResponse: { - /** - * Success - * @description Whether the policy was successfully removed - */ - success: boolean; - }; - /** - * EvaluationRequest - * @description Request model for evaluation analysis. - * - * Used to analyze agent interactions for safety violations, - * policy compliance, and control rules. - * - * Attributes: - * agent_uuid: UUID 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", - * "stage": "pre", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "type": "llm" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "post", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "output": "I cannot share sensitive payment information.", - * "type": "llm" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "pre", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "type": "tool" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "post", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "output": { - * "results": [] - * }, - * "type": "tool" - * } - * } - */ - EvaluationRequest: { - /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent making the evaluation request - */ - agent_uuid: string; - /** @description Agent step payload to evaluate */ - step: components['schemas']['Step']; - /** - * Stage - * @description Evaluation stage: 'pre' or 'post' - * @enum {string} - */ - stage: 'pre' | 'post'; - }; - /** - * EvaluationResponse - * @description Response model from evaluation analysis (server-side). - * - * This is what the server returns. The SDK may transform this - * into an EvaluationResult for client convenience. - * - * Attributes: - * is_safe: Whether the content is considered safe - * confidence: Confidence score between 0.0 and 1.0 - * reason: Optional explanation for the decision - * matches: List of controls that matched/triggered (if any) - * errors: List of controls that failed during evaluation (if any) - * 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 - */ - reason?: string | null; - /** - * 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" - * ] - * } - */ - config: { - [key: string]: unknown; - }; - }; - /** - * EvaluatorConfigItem - * @description Evaluator config template stored in the server. - */ - EvaluatorConfigItem: { - /** - * Id - * @description Evaluator config ID - */ - id: number; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Created At - * @description ISO 8601 created timestamp - */ - created_at?: string | null; - /** - * Updated At - * @description ISO 8601 updated timestamp - */ - updated_at?: string | null; - }; - /** - * EvaluatorInfo - * @description Information about a registered evaluator. - */ - EvaluatorInfo: { - /** - * Name - * @description Evaluator name - */ - name: string; - /** - * Version - * @description Evaluator version - */ - version: string; - /** - * Description - * @description Evaluator description - */ - description: string; - /** - * Requires Api Key - * @description Whether evaluator requires API key - */ - requires_api_key: boolean; - /** - * Timeout Ms - * @description Default timeout in milliseconds - */ - timeout_ms: number; - /** - * Config Schema - * @description JSON Schema for config - */ - config_schema: { - [key: string]: unknown; - }; - }; - /** - * EvaluatorResult - * @description Result from a control evaluator. - * - * The `error` field indicates evaluator failures, NOT validation failures: - * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors - * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures - * - * When `error` is set, `matched` must be False (fail-open on evaluator errors). - * When `error` is None, `matched` reflects the actual validation result. - * - * This distinction allows: - * - Clients to distinguish "data violated rules" from "evaluator is broken" - * - 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; - /** - * Message - * @description Explanation of the result - */ - message?: string | null; - /** - * Metadata - * @description Additional result metadata - */ - 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 - * @description Schema for a custom evaluator registered with an agent. - * - * Custom evaluators are Evaluator classes deployed with the engine. - * 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 - */ - config_schema?: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - }; - /** - * EvaluatorSchemaItem - * @description Evaluator schema summary for list response. - */ - EvaluatorSchemaItem: { - /** Name */ - name: string; - /** Description */ - description: string | null; - /** Config Schema */ - config_schema: { - [key: string]: unknown; - }; - }; - /** - * EventQueryRequest - * @description Request model for querying raw events. - * - * Supports filtering by various criteria and pagination. - * - * Attributes: - * 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 - * 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; - /** - * Actions - * @description Filter by actions - */ - actions?: ('allow' | 'deny' | 'warn' | 'log')[] | null; - /** - * Matched - * @description Filter by matched status - */ - matched?: boolean | null; - /** - * Check Stages - * @description Filter by check stages - */ - check_stages?: ('pre' | 'post')[] | null; - /** - * Applies To - * @description Filter by call types - */ - applies_to?: ('llm_call' | 'tool_call')[] | null; - /** - * Start Time - * @description Filter events after this time - */ - start_time?: string | null; - /** - * End Time - * @description Filter events before this time - */ - end_time?: string | null; - /** - * Limit - * @description Maximum events - * @default 100 - */ - limit: number; - /** - * Offset - * @description Pagination offset - * @default 0 - */ - offset: number; - }; - /** - * EventQueryResponse - * @description Response model for event queries. - * - * Attributes: - * events: List of matching events - * total: Total number of matching events (for pagination) - * limit: Limit used in query - * offset: Offset used in query - */ - EventQueryResponse: { - /** - * Events - * @description Matching events - */ - events: components['schemas']['ControlExecutionEvent'][]; - /** - * Total - * @description Total matching events - */ - total: number; - /** - * Limit - * @description Limit used in query - */ - limit: number; - /** - * Offset - * @description Offset used in query - */ - offset: number; - }; - /** - * GetAgentResponse - * @description Response containing agent details and registered steps. - */ - 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'][]; - }; - /** GetControlDataResponse */ - GetControlDataResponse: { - /** @description Control data payload */ - data: components['schemas']['ControlDefinition']; - }; - /** - * GetControlResponse - * @description Response containing control details. - */ - GetControlResponse: { - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - /** @description Control configuration data (None if not yet configured) */ - data?: components['schemas']['ControlDefinition'] | null; - }; - /** - * GetPolicyControlsResponse - * @description Response containing control IDs associated with a policy. - */ - GetPolicyControlsResponse: { - /** - * Control Ids - * @description List of control IDs associated with the policy - */ - control_ids: number[]; - }; - /** GetPolicyResponse */ - GetPolicyResponse: { - /** - * Policy Id - * @description Identifier of the policy assigned to the agent - */ - policy_id: number; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** - * HealthResponse - * @description Health check response model. - * - * Attributes: - * status: Current health status (e.g., "healthy", "degraded", "unhealthy") - * version: Application version - */ - HealthResponse: { - /** Status */ - status: string; - /** Version */ - version: string; - }; - /** - * 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" - * }, - * "evaluators": [ - * { - * "config_schema": { - * "properties": { - * "sensitivity": { - * "type": "string" - * } - * }, - * "type": "object" - * }, - * "description": "Detects PII in text", - * "name": "pii-detector" - * } - * ], - * "steps": [ - * { - * "input_schema": { - * "query": { - * "type": "string" - * } - * }, - * "name": "search_kb", - * "output_schema": { - * "results": { - * "type": "array" - * } - * }, - * "type": "tool" - * } - * ] - * } - */ - InitAgentRequest: { - /** @description Agent metadata including ID, name, and version */ - agent: components['schemas']['Agent']; - /** - * Steps - * @description List of steps available to the agent - */ - steps?: components['schemas']['StepSchema'][]; - /** - * Evaluators - * @description Custom evaluator schemas for config validation - */ - evaluators?: components['schemas']['EvaluatorSchema'][]; - /** - * Force Replace - * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. - * @default false - */ - force_replace: boolean; - }; - /** - * InitAgentResponse - * @description Response from agent initialization. - */ - InitAgentResponse: { - /** - * Created - * @description True if agent was newly created, False if updated - */ - created: boolean; - /** - * Controls - * @description Active protection controls for the agent (if policy assigned) - */ - controls?: components['schemas']['Control'][]; - }; - JSONObject: { - [key: string]: components['schemas']['JSONValue']; - }; - /** @description Any JSON value */ - JSONValue: unknown; - /** - * ListAgentsResponse - * @description Response for listing agents. - */ - ListAgentsResponse: { - /** - * Agents - * @description List of agent summaries - */ - agents: components['schemas']['AgentSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListControlsResponse - * @description Response for listing controls. - */ - ListControlsResponse: { - /** - * Controls - * @description List of control summaries - */ - controls: components['schemas']['ControlSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorConfigsResponse - * @description Response for listing evaluator configs. - */ - ListEvaluatorConfigsResponse: { - /** - * Evaluator Configs - * @description List of evaluator configs - */ - evaluator_configs: components['schemas']['EvaluatorConfigItem'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorsResponse - * @description Response for listing agent's evaluator schemas. - */ - ListEvaluatorsResponse: { - /** Evaluators */ - evaluators: components['schemas']['EvaluatorSchemaItem'][]; - pagination: components['schemas']['PaginationInfo']; - }; - /** - * PaginationInfo - * @description Pagination metadata for cursor-based pagination. - */ - PaginationInfo: { - /** - * 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 - */ - has_more: boolean; - }; - /** - * 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[]; - }; - /** - * 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[]; - }; - /** - * 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; - }; - /** - * PatchControlResponse - * @description Response from control metadata update. - */ - PatchControlResponse: { - /** - * Success - * @description Whether the update succeeded - */ - success: boolean; - /** - * Name - * @description Current control name (may have changed) - */ - name: string; - /** - * Enabled - * @description Current enabled status (if control has data configured) - */ - enabled?: boolean | null; - }; - /** - * SetControlDataRequest - * @description Request to update control configuration data. - */ - SetControlDataRequest: { - /** @description Control configuration data (replaces existing) */ - data: components['schemas']['ControlDefinition']; - }; - /** SetControlDataResponse */ - SetControlDataResponse: { - /** - * Success - * @description Whether the control data was updated - */ - success: boolean; - }; - /** 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; - }; - /** - * StatsResponse - * @description Response model for agent-level aggregated statistics. - * - * Contains agent-level totals (with optional timeseries) and per-control breakdown. - * - * Attributes: - * agent_uuid: Agent UUID - * 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_uuid: string; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** @description Agent-level aggregate statistics */ - totals: components['schemas']['StatsTotals']; - /** - * Controls - * @description Per-control breakdown - */ - controls: components['schemas']['ControlStats'][]; - }; - /** - * StatsTotals - * @description Agent-level aggregate statistics. - * - * Invariant: execution_count = match_count + non_match_count + error_count - * - * Matches have actions (allow, deny, warn, log) tracked in action_counts. - * sum(action_counts.values()) == match_count - * - * Attributes: - * execution_count: Total executions across all controls - * match_count: Total matches across all controls (evaluator matched) - * non_match_count: Total non-matches across all controls (evaluator didn't match) - * error_count: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions - * timeseries: Time-series data points (only when include_timeseries=true) - */ - StatsTotals: { - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Match Count - * @description Total matches - * @default 0 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - * @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) - */ - timeseries?: components['schemas']['TimeseriesBucket'][] | null; - }; - /** - * Step - * @description Runtime payload for an agent step invocation. - */ - Step: { - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; - /** - * 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; - }; - /** - * 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; - }; - /** - * StepSchema - * @description Schema for a registered agent step. - * @example { - * "description": "Search the internal knowledge base", - * "input_schema": { - * "query": { - * "description": "Search query", - * "type": "string" - * } - * }, - * "name": "search_knowledge_base", - * "output_schema": { - * "results": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "type": "tool" - * } - * @example { - * "description": "Customer support response generation", - * "input_schema": { - * "messages": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "name": "support-answer", - * "output_schema": { - * "text": { - * "type": "string" - * } - * }, - * "type": "llm" - * } - */ - 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 - */ - description?: string | null; - /** - * Input Schema - * @description JSON schema describing step input - */ - input_schema?: { - [key: string]: unknown; - } | null; - /** - * Output Schema - * @description JSON schema describing step output - */ - output_schema?: { - [key: string]: unknown; - } | null; - /** - * Metadata - * @description Additional metadata for the step - */ - metadata?: { - [key: string]: unknown; - } | null; - }; - /** - * TimeseriesBucket - * @description Single data point in a time-series. - * - * Represents aggregated metrics for a single time bucket. - * - * Attributes: - * timestamp: Start time of the bucket (UTC, always timezone-aware) - * execution_count: Total executions in this bucket - * match_count: Number of matches in this bucket - * non_match_count: Number of non-matches in this bucket - * error_count: Number of errors in this bucket - * action_counts: Breakdown of actions for matched executions - * avg_confidence: Average confidence score (None if no executions) - * avg_duration_ms: Average execution duration in milliseconds (None if no data) - */ - TimeseriesBucket: { - /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) - */ - timestamp: string; - /** - * Execution Count - * @description Total executions in bucket - */ - execution_count: number; - /** - * Match Count - * @description Matches in bucket - */ - match_count: number; - /** - * Non Match Count - * @description Non-matches in bucket - */ - 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) - */ - avg_duration_ms?: number | null; - }; - /** - * UpdateEvaluatorConfigRequest - * @description Request to replace an evaluator config template. - */ - UpdateEvaluatorConfigRequest: { - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - }; - /** ValidationError */ - ValidationError: { - /** Location */ - loc: (string | number)[]; - /** Message */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * Agent + * @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. + * @example { + * "agent_description": "Handles customer inquiries and support tickets", + * "agent_id": "550e8400-e29b-41d4-a716-446655440000", + * "agent_metadata": { + * "environment": "production", + * "team": "support" + * }, + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * } + */ + Agent: { + /** + * Agent Created At + * @description ISO 8601 timestamp when agent was created + */ + agent_created_at?: string | null; + /** + * Agent Description + * @description Optional description of the agent's purpose + */ + agent_description?: string | null; + /** + * Agent Id + * Format: uuid + * @description Unique identifier for the agent (UUID format) + */ + agent_id: string; + /** + * Agent Metadata + * @description Free-form metadata dictionary for custom properties + */ + agent_metadata?: { + [key: string]: unknown; + } | null; + /** + * Agent Name + * @description Human-readable name for the agent + */ + agent_name: string; + /** + * Agent Updated At + * @description ISO 8601 timestamp when agent was last updated + */ + agent_updated_at?: string | null; + /** + * Agent Version + * @description Semantic version string (e.g. '1.0.0') + */ + agent_version?: string | null; + }; + /** AgentControlsResponse */ + AgentControlsResponse: { + /** + * Controls + * @description List of active controls associated with the agent + */ + controls: components["schemas"]["Control"][]; + }; + /** + * AgentRef + * @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 + */ + agent_name: string; + }; + /** + * AgentSummary + * @description Summary of an agent for list responses. + */ + AgentSummary: { + /** + * Active Controls Count + * @description Number of active controls for this agent + * @default 0 + */ + active_controls_count: number; + /** + * Agent Id + * @description UUID of the agent + */ + agent_id: string; + /** + * Agent Name + * @description Human-readable name of the agent + */ + agent_name: string; + /** + * Created At + * @description ISO 8601 timestamp when agent was created + */ + created_at?: string | null; + /** + * Evaluator Count + * @description Number of evaluators registered with the agent + * @default 0 + */ + evaluator_count: number; + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + /** + * Step Count + * @description Number of steps registered with the agent + * @default 0 + */ + step_count: number; + }; + /** AssocResponse */ + AssocResponse: { + /** + * Success + * @description Whether the association change succeeded + */ + success: boolean; + }; + /** + * BatchEventsRequest + * @description Request model for batch event ingestion. + * + * SDKs batch events and send them to the server periodically. + * This reduces HTTP overhead significantly (100x reduction). + * + * Attributes: + * events: List of control execution events to ingest + * @example { + * "events": [ + * { + * "action": "deny", + * "agent_name": "my-agent", + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_id": 123, + * "control_name": "sql-injection-check", + * "matched": true, + * "span_id": "00f067aa0ba902b7", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * ] + * } + */ + BatchEventsRequest: { + /** + * Events + * @description List of events to ingest + */ + events: components["schemas"]["ControlExecutionEvent"][]; + }; + /** + * BatchEventsResponse + * @description Response model for batch event ingestion. + * + * Attributes: + * received: Number of events received + * enqueued: Number of events successfully enqueued + * dropped: Number of events dropped (queue full) + * status: Overall status ('queued', 'partial', 'failed') + */ + BatchEventsResponse: { + /** + * Dropped + * @description Number of events dropped + */ + dropped: number; + /** + * Enqueued + * @description Number of events enqueued + */ + enqueued: number; + /** + * Received + * @description Number of events received + */ + received: number; + /** + * Status + * @description Overall ingestion status + * @enum {string} + */ + status: "queued" | "partial" | "failed"; + }; + /** + * Control + * @description A control with identity and configuration. + * + * Note: Only fully-configured controls (with valid ControlDefinition) + * are returned from API endpoints. Unconfigured controls are filtered out. + */ + Control: { + control: components["schemas"]["ControlDefinition"]; + /** Id */ + id: number; + /** Name */ + name: string; + }; + /** + * ControlAction + * @description What to do when control matches. + */ + ControlAction: { + /** + * Decision + * @description Action to take when control is triggered + * @enum {string} + */ + decision: "allow" | "deny" | "warn" | "log"; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + ControlDefinition: { + /** @description What action to take when control matches */ + action: components["schemas"]["ControlAction"]; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components["schemas"]["EvaluatorSpec"]; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: "server" | "sdk"; + /** @description Which steps and stages this control applies to */ + scope?: components["schemas"]["ControlScope"]; + /** @description What data to select from the payload */ + selector: components["schemas"]["ControlSelector"]; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlExecutionEvent + * @description Represents a single control execution event. + * + * This is the core observability data model, capturing: + * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) + * - Context: agent, control, check stage, applies to + * - Result: action taken, whether matched, confidence score + * - Timing: when it happened, how long it took + * - Optional details: evaluator name, selector path, errors, metadata + * + * Attributes: + * 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) + * 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) + * applies_to: "llm_call" or "tool_call" + * action: The action taken (allow, deny, warn, log) + * matched: Whether the control evaluator matched + * confidence: Confidence score from the evaluator (0.0-1.0) + * timestamp: When the control was executed (UTC) + * execution_duration_ms: How long the control evaluation took + * evaluator_name: Name of the evaluator used + * selector_path: The selector path used to extract data + * error_message: Error message if evaluation failed + * metadata: Additional metadata for extensibility + * @example { + * "action": "deny", + * "agent_name": "my-agent", + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", + * "control_id": 123, + * "control_name": "sql-injection-check", + * "evaluator_name": "regex", + * "execution_duration_ms": 15.3, + * "matched": true, + * "selector_path": "input", + * "span_id": "00f067aa0ba902b7", + * "timestamp": "2025-01-09T10:30:00Z", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + */ + ControlExecutionEvent: { + /** + * Action + * @description Action taken by the control + * @enum {string} + */ + action: "allow" | "deny" | "warn" | "log"; + /** + * Agent Name + * @description Name of the agent (denormalized) + */ + agent_name: string; + /** + * Agent Uuid + * Format: uuid + * @description UUID of the agent + */ + agent_uuid: string; + /** + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} + */ + applies_to: "llm_call" | "tool_call"; + /** + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} + */ + check_stage: "pre" | "post"; + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control (denormalized) + */ + control_name: string; + /** + * Error Message + * @description Error message if evaluation failed + */ + error_message?: string | null; + /** + * Evaluator Name + * @description Name of the evaluator used + */ + evaluator_name?: string | null; + /** + * Execution Duration Ms + * @description Execution duration in milliseconds + */ + execution_duration_ms?: number | null; + /** + * Matched + * @description Whether the evaluator matched (True) or not (False) + */ + matched: boolean; + /** + * Metadata + * @description Additional metadata + */ + metadata?: { + [key: string]: unknown; + }; + /** + * Selector Path + * @description Selector path used to extract data + */ + selector_path?: string | null; + /** + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + */ + span_id: string; + /** + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) + */ + 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) + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control + */ + control_name: string; + /** @description Evaluator result (confidence, message, metadata) */ + result: components["schemas"]["EvaluatorResult"]; + }; + /** + * ControlScope + * @description Defines when a control applies to a Step. + * @example { + * "stages": [ + * "pre" + * ], + * "step_types": [ + * "tool" + * ] + * } + * @example { + * "step_names": [ + * "search_db", + * "fetch_user" + * ] + * } + * @example { + * "step_name_regex": "^db_.*" + * } + * @example { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * } + */ + ControlScope: { + /** + * Stages + * @description Evaluation stages this control applies to + */ + stages?: ("pre" | "post")[] | null; + /** + * Step Name Regex + * @description RE2 pattern matched with search() against step name + */ + step_name_regex?: string | null; + /** + * Step Names + * @description Exact step names this control applies to + */ + 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 + * @description Selects data from a Step payload. + * + * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" + * meaning the entire Step object. + * @example { + * "path": "output" + * } + * @example { + * "path": "context.user_id" + * } + * @example { + * "path": "input" + * } + * @example { + * "path": "*" + * } + * @example { + * "path": "name" + * } + * @example { + * "path": "output" + * } + */ + ControlSelector: { + /** + * Path + * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' + * @default * + */ + path: string | null; + }; + /** + * ControlStats + * @description Aggregated statistics for a single control. + * + * Attributes: + * control_id: Database ID of the control + * control_name: Name of the control + * execution_count: Total number of executions + * match_count: Number of times the control matched + * non_match_count: Number of times the control did not match + * allow_count: Number of allow actions + * deny_count: Number of deny actions + * warn_count: Number of warn actions + * log_count: Number of log actions + * error_count: Number of errors during evaluation + * avg_confidence: Average confidence score + * 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 + */ + control_id: number; + /** + * Control Name + * @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 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + */ + non_match_count: number; + /** + * Warn Count + * @description Warn actions + */ + warn_count: number; + }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_uuid: Agent UUID + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Uuid + * Format: uuid + * @description Agent UUID + */ + agent_uuid: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + 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: { + /** + * Description + * @description Control description + */ + description?: string | null; + /** + * Enabled + * @description Whether control is enabled + * @default true + */ + enabled: boolean; + /** + * Execution + * @description 'server' or 'sdk' + */ + execution?: string | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + /** + * Stages + * @description Evaluation stages in scope + */ + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; + /** + * Tags + * @description Control tags + */ + tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components["schemas"]["AgentRef"] | null; + }; + /** CreateControlRequest */ + CreateControlRequest: { + /** + * Name + * @description Unique control name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreateControlResponse */ + CreateControlResponse: { + /** + * Control Id + * @description Identifier of the created control + */ + control_id: number; + }; + /** + * CreateEvaluatorConfigRequest + * @description Request to create an evaluator config template. + */ + CreateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyRequest */ + CreatePolicyRequest: { + /** + * Name + * @description Unique policy name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyResponse */ + CreatePolicyResponse: { + /** + * Policy Id + * @description Identifier of the created policy + */ + policy_id: number; + }; + /** + * DeleteControlResponse + * @description Response for deleting a control. + */ + DeleteControlResponse: { + /** + * Dissociated From Agents + * @description Agent IDs the control was removed from before deletion + */ + dissociated_from_agents?: string[]; + /** + * Dissociated From Policies + * @description Policy IDs the control was removed from before deletion + */ + dissociated_from_policies?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; + }; + /** + * DeleteEvaluatorConfigResponse + * @description Response for deleting an evaluator config. + */ + DeleteEvaluatorConfigResponse: { + /** + * Success + * @description Whether the evaluator config was deleted + */ + success: boolean; + }; + /** + * EvaluationRequest + * @description Request model for evaluation analysis. + * + * Used to analyze agent interactions for safety violations, + * policy compliance, and control rules. + * + * Attributes: + * agent_uuid: UUID 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", + * "stage": "pre", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "type": "llm" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "post", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "output": "I cannot share sensitive payment information.", + * "type": "llm" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "pre", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "type": "tool" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "post", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "output": { + * "results": [] + * }, + * "type": "tool" + * } + * } + */ + EvaluationRequest: { + /** + * Agent Uuid + * Format: uuid + * @description UUID of the agent making the evaluation request + */ + agent_uuid: string; + /** + * Stage + * @description Evaluation stage: 'pre' or 'post' + * @enum {string} + */ + stage: "pre" | "post"; + /** @description Agent step payload to evaluate */ + step: components["schemas"]["Step"]; + }; + /** + * EvaluationResponse + * @description Response model from evaluation analysis (server-side). + * + * This is what the server returns. The SDK may transform this + * into an EvaluationResult for client convenience. + * + * Attributes: + * is_safe: Whether the content is considered safe + * confidence: Confidence score between 0.0 and 1.0 + * reason: Optional explanation for the decision + * matches: List of controls that matched/triggered (if any) + * errors: List of controls that failed during evaluation (if any) + * non_matches: List of controls that were evaluated but did not match (if any) + */ + EvaluationResponse: { + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Errors + * @description List of controls that failed during evaluation (if any) + */ + 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; + /** + * Non Matches + * @description List of controls that were evaluated but did not match (if any) + */ + non_matches?: components["schemas"]["ControlMatch"][] | null; + /** + * Reason + * @description Explanation for the decision + */ + reason?: string | null; + }; + /** + * EvaluatorConfigItem + * @description Evaluator config template stored in the server. + */ + EvaluatorConfigItem: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Created At + * @description ISO 8601 created timestamp + */ + created_at?: string | null; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Id + * @description Evaluator config ID + */ + id: number; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + /** + * Updated At + * @description ISO 8601 updated timestamp + */ + updated_at?: string | null; + }; + /** + * EvaluatorInfo + * @description Information about a registered evaluator. + */ + EvaluatorInfo: { + /** + * Config Schema + * @description JSON Schema for config + */ + 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 + */ + requires_api_key: boolean; + /** + * Timeout Ms + * @description Default timeout in milliseconds + */ + timeout_ms: number; + /** + * Version + * @description Evaluator version + */ + version: string; + }; + /** + * EvaluatorResult + * @description Result from a control evaluator. + * + * The `error` field indicates evaluator failures, NOT validation failures: + * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors + * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures + * + * When `error` is set, `matched` must be False (fail-open on evaluator errors). + * When `error` is None, `matched` reflects the actual validation result. + * + * This distinction allows: + * - Clients to distinguish "data violated rules" from "evaluator is broken" + * - Observability systems to monitor evaluator health separately from validation outcomes + */ + EvaluatorResult: { + /** + * 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 + */ + message?: string | null; + /** + * Metadata + * @description Additional result metadata + */ + metadata?: { + [key: string]: unknown; + } | null; + }; + /** + * EvaluatorSchema + * @description Schema for a custom evaluator registered with an agent. + * + * Custom evaluators are Evaluator classes deployed with the engine. + * This schema is registered via initAgent for validation and UI purposes. + */ + EvaluatorSchema: { + /** + * Config Schema + * @description JSON Schema for evaluator config validation + */ + config_schema?: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; + }; + /** + * EvaluatorSchemaItem + * @description Evaluator schema summary for list response. + */ + EvaluatorSchemaItem: { + /** 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 + * @description Request model for querying raw events. + * + * Supports filtering by various criteria and pagination. + * + * Attributes: + * 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 + * 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: { + /** + * Actions + * @description Filter by actions + */ + actions?: ("allow" | "deny" | "warn" | "log")[] | null; + /** + * Agent Uuid + * @description Filter by agent UUID + */ + agent_uuid?: 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; + /** + * Control Execution Id + * @description Filter by specific event ID + */ + control_execution_id?: string | null; + /** + * Control Ids + * @description Filter by control IDs + */ + control_ids?: number[] | null; + /** + * End Time + * @description Filter events before this time + */ + end_time?: string | null; + /** + * Limit + * @description Maximum events + * @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 + * @description Response model for event queries. + * + * Attributes: + * events: List of matching events + * total: Total number of matching events (for pagination) + * limit: Limit used in query + * offset: Offset used in query + */ + EventQueryResponse: { + /** + * Events + * @description Matching events + */ + events: components["schemas"]["ControlExecutionEvent"][]; + /** + * Limit + * @description Limit used in query + */ + limit: number; + /** + * Offset + * @description Offset used in query + */ + offset: number; + /** + * Total + * @description Total matching events + */ + total: number; + }; + /** GetAgentPoliciesResponse */ + GetAgentPoliciesResponse: { + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + }; + /** + * GetAgentResponse + * @description Response containing agent details and registered steps. + */ + GetAgentResponse: { + /** @description Agent metadata */ + agent: components["schemas"]["Agent"]; + /** + * 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: { + /** @description Control data payload */ + data: components["schemas"]["ControlDefinition"]; + }; + /** + * GetControlResponse + * @description Response containing control details. + */ + GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components["schemas"]["ControlDefinition"] | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + }; + /** + * GetPolicyControlsResponse + * @description Response containing control IDs associated with a policy. + */ + GetPolicyControlsResponse: { + /** + * Control Ids + * @description List of control IDs associated with the policy + */ + control_ids: number[]; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * HealthResponse + * @description Health check response model. + * + * Attributes: + * status: Current health status (e.g., "healthy", "degraded", "unhealthy") + * version: Application version + */ + HealthResponse: { + /** Status */ + status: string; + /** Version */ + version: string; + }; + /** + * 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" + * }, + * "evaluators": [ + * { + * "config_schema": { + * "properties": { + * "sensitivity": { + * "type": "string" + * } + * }, + * "type": "object" + * }, + * "description": "Detects PII in text", + * "name": "pii-detector" + * } + * ], + * "steps": [ + * { + * "input_schema": { + * "query": { + * "type": "string" + * } + * }, + * "name": "search_kb", + * "output_schema": { + * "results": { + * "type": "array" + * } + * }, + * "type": "tool" + * } + * ] + * } + */ + InitAgentRequest: { + /** @description Agent metadata including ID, name, and version */ + agent: components["schemas"]["Agent"]; + /** + * Evaluators + * @description Custom evaluator schemas for config validation + */ + evaluators?: components["schemas"]["EvaluatorSchema"][]; + /** + * Force Replace + * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. + * @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 + */ + controls?: components["schemas"]["Control"][]; + /** + * Created + * @description True if agent was newly created, False if updated + */ + created: boolean; + }; + JSONObject: { + [key: string]: components["schemas"]["JSONValue"]; + }; + /** @description Any JSON value */ + JSONValue: unknown; + /** + * ListAgentsResponse + * @description Response for listing agents. + */ + ListAgentsResponse: { + /** + * Agents + * @description List of agent summaries + */ + agents: components["schemas"]["AgentSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListControlsResponse + * @description Response for listing controls. + */ + ListControlsResponse: { + /** + * Controls + * @description List of control summaries + */ + controls: components["schemas"]["ControlSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorConfigsResponse + * @description Response for listing evaluator configs. + */ + ListEvaluatorConfigsResponse: { + /** + * Evaluator Configs + * @description List of evaluator configs + */ + evaluator_configs: components["schemas"]["EvaluatorConfigItem"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorsResponse + * @description Response for listing agent's evaluator schemas. + */ + ListEvaluatorsResponse: { + /** Evaluators */ + evaluators: components["schemas"]["EvaluatorSchemaItem"][]; + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * PaginationInfo + * @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; + /** + * Next Cursor + * @description Cursor for fetching the next page (null if no more pages) + */ + next_cursor?: string | null; + /** + * Total + * @description Total number of items + */ + total: number; + }; + /** + * PatchAgentRequest + * @description Request to modify an agent (remove steps/evaluators). + */ + PatchAgentRequest: { + /** + * 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: { + /** + * 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: { + /** + * Enabled + * @description Enable or disable the control + */ + enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; + }; + /** + * PatchControlResponse + * @description Response from control metadata update. + */ + PatchControlResponse: { + /** + * Enabled + * @description Current enabled status (if control has data configured) + */ + enabled?: boolean | null; + /** + * Name + * @description Current control name (may have changed) + */ + name: string; + /** + * Success + * @description Whether the update succeeded + */ + success: boolean; + }; + /** + * SetControlDataRequest + * @description Request to update control configuration data. + */ + SetControlDataRequest: { + /** @description Control configuration data (replaces existing) */ + data: components["schemas"]["ControlDefinition"]; + }; + /** SetControlDataResponse */ + SetControlDataResponse: { + /** + * Success + * @description Whether the control data was updated + */ + success: boolean; + }; + /** + * StatsResponse + * @description Response model for agent-level aggregated statistics. + * + * Contains agent-level totals (with optional timeseries) and per-control breakdown. + * + * Attributes: + * agent_uuid: Agent UUID + * 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_uuid: string; + /** + * Controls + * @description Per-control breakdown + */ + controls: components["schemas"]["ControlStats"][]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components["schemas"]["StatsTotals"]; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * 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 + */ + execution_count: number; + /** + * Match Count + * @description Total matches + * @default 0 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + * @default 0 + */ + non_match_count: number; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components["schemas"]["TimeseriesBucket"][] | null; + }; + /** + * Step + * @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 Output content for this step (None for pre-checks) */ + output?: components["schemas"]["JSONValue"] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * StepKey + * @description Identifies a registered step schema by type and name. + */ + StepKey: { + /** + * Name + * @description Registered step name + */ + name: string; + /** + * Type + * @description Step type + */ + type: string; + }; + /** + * StepSchema + * @description Schema for a registered agent step. + * @example { + * "description": "Search the internal knowledge base", + * "input_schema": { + * "query": { + * "description": "Search query", + * "type": "string" + * } + * }, + * "name": "search_knowledge_base", + * "output_schema": { + * "results": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "type": "tool" + * } + * @example { + * "description": "Customer support response generation", + * "input_schema": { + * "messages": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "name": "support-answer", + * "output_schema": { + * "text": { + * "type": "string" + * } + * }, + * "type": "llm" + * } + */ + StepSchema: { + /** + * Description + * @description Optional description of the step + */ + description?: string | null; + /** + * Input Schema + * @description JSON schema describing step input + */ + 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 + */ + output_schema?: { + [key: string]: unknown; + } | null; + /** + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * 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) + */ + avg_duration_ms?: number | null; + /** + * Error Count + * @description Errors in bucket + */ + error_count: number; + /** + * Execution Count + * @description Total executions in bucket + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + }; + /** + * UpdateEvaluatorConfigRequest + * @description Request to replace an evaluator config template. + */ + UpdateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + 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 */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - list_agents_api_v1_agents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - name?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of agent summaries */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListAgentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - init_agent_api_v1_agents_initAgent_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['InitAgentRequest']; - }; - }; - responses: { - /** @description Agent registration status with active controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['InitAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_api_v1_agents__agent_id__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Agent metadata and registered steps */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_agent_api_v1_agents__agent_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchAgentRequest']; - }; - }; - responses: { - /** @description Lists of removed items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of controls from agent's policy */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AgentControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_evaluators_api_v1_agents__agent_id__evaluators_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schemas registered with this agent */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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: { - 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']; - }; - }; - }; - }; - 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']; - }; - }; - }; - }; - 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']; - }; - }; - }; - }; - list_controls_api_v1_controls_get: { - parameters: { - query?: { - /** @description Control ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by enabled status */ - enabled?: boolean | null; - /** @description Filter by step type (built-ins: 'tool', 'llm') */ - step_type?: string | null; - /** @description Filter by stage ('pre' or 'post') */ - stage?: string | null; - /** @description Filter by execution ('server' or 'sdk') */ - execution?: string | null; - /** @description Filter by tag */ - tag?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_control_api_v1_controls_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateControlRequest']; - }; - }; - responses: { - /** @description Created control ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CreateControlResponse']; - }; - }; - /** @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; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control metadata and configuration */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_control_api_v1_controls__control_id__delete: { - parameters: { - query?: { - /** @description If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. */ - force?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation with dissociation info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_control_api_v1_controls__control_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchControlRequest']; - }; - }; - responses: { - /** @description Updated control information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_data_api_v1_controls__control_id__data_get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control data payload */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - set_control_data_api_v1_controls__control_id__data_put: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SetControlDataRequest']; - }; - }; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_evaluator_configs_api_v1_evaluator_configs_get: { - parameters: { - query?: { - /** @description Evaluator config ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by evaluator name */ - evaluator?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of evaluator configs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorConfigsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_evaluator_config_api_v1_evaluator_configs_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Created evaluator config */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_evaluator_config_api_v1_evaluator_configs__config_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator config details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - update_evaluator_config_api_v1_evaluator_configs__config_id__put: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Updated evaluator config */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteEvaluatorConfigResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Dictionary of evaluator name to evaluator info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: components['schemas']['EvaluatorInfo']; - }; - }; - }; - }; - }; - ingest_events_api_v1_observability_events_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['BatchEventsRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BatchEventsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - query_events_api_v1_observability_events_query_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EventQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EventQueryResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_stats_api_v1_observability_stats_get: { - parameters: { - query: { - agent_uuid: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_stats_api_v1_observability_stats_controls__control_id__get: { - parameters: { - query: { - agent_uuid: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ControlStatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_status_api_v1_observability_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: unknown; - }; - }; - }; - }; - }; - health_check_health_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server health status */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; - }; - }; - }; - }; + list_agents_api_v1_agents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + name?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of agent summaries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListAgentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + init_agent_api_v1_agents_initAgent_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InitAgentRequest"]; + }; + }; + responses: { + /** @description Agent registration status with active controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InitAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_api_v1_agents__agent_id__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Agent metadata and registered steps */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_agent_api_v1_agents__agent_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchAgentRequest"]; + }; + }; + responses: { + /** @description Lists of removed items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_agent_controls_api_v1_agents__agent_id__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of controls from agent policy and direct associations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_agent_control_api_v1_agents__agent_id__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + 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_agent_control_api_v1_agents__agent_id__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + 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"]; + }; + }; + }; + }; + list_agent_evaluators_api_v1_agents__agent_id__evaluators_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schemas registered with this agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + 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"]; + }; + }; + }; + }; + get_agent_policies_api_v1_agents__agent_id__policies_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of policy IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentPoliciesResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_all_agent_policies_api_v1_agents__agent_id__policies_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"]["AssocResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + policy_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_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + policy_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"]; + }; + }; + }; + }; + list_controls_api_v1_controls_get: { + parameters: { + query?: { + /** @description Control ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by enabled status */ + enabled?: boolean | null; + /** @description Filter by step type (built-ins: 'tool', 'llm') */ + step_type?: string | null; + /** @description Filter by stage ('pre' or 'post') */ + stage?: string | null; + /** @description Filter by execution ('server' or 'sdk') */ + execution?: string | null; + /** @description Filter by tag */ + tag?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_control_api_v1_controls_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateControlRequest"]; + }; + }; + responses: { + /** @description Created control ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + 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; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control metadata and configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_control_api_v1_controls__control_id__delete: { + parameters: { + query?: { + /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + force?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation with dissociation info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_control_api_v1_controls__control_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchControlRequest"]; + }; + }; + responses: { + /** @description Updated control information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_data_api_v1_controls__control_id__data_get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control data payload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_control_data_api_v1_controls__control_id__data_put: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetControlDataRequest"]; + }; + }; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + 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?: { + /** @description Evaluator config ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by evaluator name */ + evaluator?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of evaluator configs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorConfigsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_evaluator_config_api_v1_evaluator_configs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Created evaluator config */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluator_config_api_v1_evaluator_configs__config_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator config details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_evaluator_config_api_v1_evaluator_configs__config_id__put: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Updated evaluator config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteEvaluatorConfigResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluators_api_v1_evaluators_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dictionary of evaluator name to evaluator info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: components["schemas"]["EvaluatorInfo"]; + }; + }; + }; + }; + }; + ingest_events_api_v1_observability_events_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BatchEventsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchEventsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + query_events_api_v1_observability_events_query_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventQueryRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventQueryResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_stats_api_v1_observability_stats_get: { + parameters: { + query: { + agent_uuid: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_uuid: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ControlStatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_status_api_v1_observability_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; + 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; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server health status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; } diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index 2a4e173d..9992aba2 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -13,11 +13,9 @@ type AddControlToAgentParams = { /** * Mutation hook to add a control to an agent * Flow: - * 1. Check if agent has a policy - * 2. If no policy, create one and assign it to the agent - * 3. Create the control - * 4. Set control data (definition) - * 5. Add control to policy + * 1. Create the control + * 2. Set control data (definition) + * 3. Associate the control directly with the agent */ export function useAddControlToAgent() { const queryClient = useQueryClient(); @@ -28,46 +26,7 @@ export function useAddControlToAgent() { controlName, definition, }: AddControlToAgentParams) => { - // Step 1: Check if agent has a policy - let policyId: number; - const { data: policyData, error: policyError } = - await api.agents.getPolicy(agentId); - - if (policyError || !policyData?.policy_id) { - // Step 2: Create a new policy and assign it to the agent - const policyName = `policy-${agentId}`; - const { - data: createPolicyResult, - error: createPolicyError, - response: createPolicyResponse, - } = await api.policies.create(policyName); - - if (createPolicyError || !createPolicyResult) { - throw parseApiError( - createPolicyError, - 'Failed to create policy', - createPolicyResponse?.status - ); - } - - policyId = createPolicyResult.policy_id; - - // Assign policy to agent - const { error: assignError, response: assignResponse } = - await api.agents.setPolicy(agentId, policyId); - - if (assignError) { - throw parseApiError( - assignError, - 'Failed to assign policy to agent', - assignResponse?.status - ); - } - } else { - policyId = policyData.policy_id; - } - - // Step 3: Create the control + // Step 1: Create the control const { data: createControlResult, error: createControlError, @@ -84,7 +43,7 @@ export function useAddControlToAgent() { const controlId = createControlResult.control_id; - // Step 4: Set control data (definition) + // Step 2: Set control data (definition) const { error: setDataError, response: setDataResponse } = await api.controls.setData(controlId, { data: definition, @@ -98,19 +57,19 @@ export function useAddControlToAgent() { ); } - // Step 5: Add control to policy - const { error: addControlError, response: addControlResponse } = - await api.policies.addControl(policyId, controlId); + // Step 3: Associate control directly with the agent + const { error: associateError, response: associateResponse } = + await api.agents.addControl(agentId, controlId); - if (addControlError) { + if (associateError) { throw parseApiError( - addControlError, - 'Failed to add control to policy', - addControlResponse?.status + associateError, + 'Failed to associate control with agent', + associateResponse?.status ); } - return { controlId, policyId }; + return { controlId }; }, onSuccess: (_data, variables) => { // Invalidate relevant queries to refetch data diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-delete-control.ts index 719efab4..78139ea2 100644 --- a/ui/src/core/hooks/query-hooks/use-delete-control.ts +++ b/ui/src/core/hooks/query-hooks/use-delete-control.ts @@ -6,13 +6,13 @@ import { parseApiError } from '@/core/api/errors'; type DeleteControlParams = { agentId: string; controlId: number; - /** If true, dissociate from all policies before deleting. Default true so control can be removed from agent. */ + /** If true, dissociate from all policy and agent links before deleting. */ force?: boolean; }; /** * Mutation hook to delete a control. - * Use force: true when deleting from agent detail so the control is removed from the policy and then deleted. + * Use force: true when deleting from agent detail so linked associations are removed first. */ export function useDeleteControl() { const queryClient = useQueryClient(); diff --git a/ui/tests/control-store.spec.ts b/ui/tests/control-store.spec.ts index 03f9c2bf..17f95204 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -478,22 +478,15 @@ test.describe('Modal Routing', () => { }); await expect(createModal).toBeVisible(); - // Mock successful API response for control creation - // Agent already has a policy (return 200 with policy_id) + // Mock successful API response for direct agent-control association await mockedPage.route( - '**/api/v1/agents/*/policy', + '**/api/v1/agents/*/controls/*', async (route, request) => { - if (request.method() === 'GET') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ policy_id: 1 }), - }); - } else if (request.method() === 'POST') { + if (request.method() === 'POST') { await route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify({}), + body: JSON.stringify({ success: true }), }); } else { await route.continue(); @@ -528,21 +521,6 @@ test.describe('Modal Routing', () => { } ); - await mockedPage.route( - '**/api/v1/policies/*/controls/*', - async (route, request) => { - if (request.method() === 'POST') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - } else { - await route.continue(); - } - } - ); - // Fill out the form and submit const controlNameInput = createModal.getByPlaceholder('Enter control name'); await controlNameInput.fill('Test Control'); @@ -568,7 +546,7 @@ test.describe('Modal Routing', () => { // Start waiting for API response before clicking (must be set up before the action) const responsePromise = mockedPage.waitForResponse( - '**/api/v1/policies/*/controls/*', + '**/api/v1/agents/*/controls/*', { timeout: 10000 } ); await confirmButton.click(); @@ -611,13 +589,20 @@ test.describe('Modal Routing', () => { await expect(controlNameInput).toHaveValue(/.*-copy$/, { timeout: 5000 }); // Set up mock routes for control creation flow (copying creates a new control) - await mockedPage.route('**/api/v1/agents/*/policy', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ policy_id: 1 }), - }); - }); + await mockedPage.route( + '**/api/v1/agents/*/controls/*', + async (route, request) => { + if (request.method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + } else { + await route.continue(); + } + } + ); await mockedPage.route('**/api/v1/controls', async (route, request) => { if (request.method() === 'PUT') { @@ -646,21 +631,6 @@ test.describe('Modal Routing', () => { } ); - await mockedPage.route( - '**/api/v1/policies/*/controls/*', - async (route, request) => { - if (request.method() === 'POST') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({}), - }); - } else { - await route.continue(); - } - } - ); - // Submit the form const saveButton = editModal.getByRole('button', { name: /Save|Create/i }); await saveButton.click(); @@ -674,7 +644,7 @@ test.describe('Modal Routing', () => { // Start waiting for API response before clicking (must be set up before the action) const responsePromise = mockedPage.waitForResponse( - '**/api/v1/policies/*/controls/*', + '**/api/v1/agents/*/controls/*', { timeout: 10000 } ); await confirmButton.click(); diff --git a/ui/tests/fixtures.ts b/ui/tests/fixtures.ts index de77e844..942a50d6 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -22,7 +22,7 @@ const agentsList: AgentSummary[] = [ { agent_id: 'agent-1', agent_name: 'Customer Support Bot', - policy_id: 1, + policy_ids: [1], created_at: '2024-01-01T00:00:00Z', step_count: 5, evaluator_count: 2, @@ -31,7 +31,7 @@ const agentsList: AgentSummary[] = [ { agent_id: 'agent-2', agent_name: 'Data Analysis Agent', - policy_id: 2, + policy_ids: [2], created_at: '2024-01-02T00:00:00Z', step_count: 3, evaluator_count: 1, @@ -40,7 +40,7 @@ const agentsList: AgentSummary[] = [ { agent_id: 'agent-3', agent_name: 'Code Review Assistant', - policy_id: 3, + policy_ids: [3], created_at: '2024-01-03T00:00:00Z', step_count: 8, evaluator_count: 4, From a071c84061020d77e11b75f2003a94b6f9045bad Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 14:14:42 -0500 Subject: [PATCH 02/32] fix: make agent control removal non-destructive and drop sdk aliases --- sdks/python/src/agent_control/agents.py | 24 ---------- sdks/python/tests/test_agent_id_validation.py | 22 +++++++-- server/tests/test_init_agent.py | 46 +++++++++++++++++++ .../hooks/query-hooks/use-delete-control.ts | 23 +++++----- .../agent-detail/agent-detail.tsx | 4 +- .../agent-detail/controls/table-columns.tsx | 16 +++---- .../controls/use-delete-control-flow.tsx | 23 +++++----- 7 files changed, 97 insertions(+), 61 deletions(-) diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index acdb3104..7d76ffc9 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -175,20 +175,6 @@ async def get_agent_policies( return cast(dict[str, Any], response.json()) -async def get_agent_policy( - client: AgentControlClient, - agent_id: str | UUID, -) -> dict[str, Any]: - """ - Backward-compatible alias for get_agent_policies(). - - Returns: - Dictionary containing: - - policy_ids: IDs of policies associated with the agent - """ - return await get_agent_policies(client, agent_id) - - async def add_agent_policy( client: AgentControlClient, agent_id: str | UUID, @@ -248,16 +234,6 @@ async def remove_agent_policies( return cast(dict[str, Any], response.json()) -async def remove_agent_policy( - client: AgentControlClient, - agent_id: str | UUID, -) -> dict[str, Any]: - """ - Backward-compatible alias for remove_agent_policies(). - """ - return await remove_agent_policies(client, agent_id) - - async def add_agent_control( client: AgentControlClient, agent_id: str | UUID, diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py index 488538ac..0bcdeaa5 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -29,25 +29,39 @@ async def test_get_agent_rejects_invalid_uuid() -> None: @pytest.mark.asyncio -async def test_get_agent_policy_rejects_invalid_uuid() -> None: +async def test_get_agent_policies_rejects_invalid_uuid() -> None: client = MagicMock() client.http_client = MagicMock() client.http_client.get = AsyncMock() with pytest.raises(ValueError, match="agent_id must be a valid UUID"): - await agents.get_agent_policy(client, "not-a-uuid") + await agents.get_agent_policies(client, "not-a-uuid") 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_policies_rejects_invalid_uuid() -> None: client = MagicMock() client.http_client = MagicMock() client.http_client.delete = AsyncMock() with pytest.raises(ValueError, match="agent_id must be a valid UUID"): - await agents.remove_agent_policy(client, "not-a-uuid") + await agents.remove_agent_policies(client, "not-a-uuid") + + client.http_client.delete.assert_not_called() + + +@pytest.mark.asyncio +async def test_remove_agent_policy_association_rejects_invalid_uuid() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock() + + with pytest.raises(ValueError, match="agent_id must be a valid UUID"): + await agents.remove_agent_policy_association( + client, "not-a-uuid", policy_id=1 + ) client.http_client.delete.assert_not_called() diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index 656f1329..102b31f1 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -422,6 +422,52 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: ) +def test_add_and_remove_direct_agent_control_does_not_delete_global_control( + client: TestClient, +) -> None: + # Given: an agent and a configured control + payload = make_agent_payload() + init_resp = client.post("/api/v1/agents/initAgent", json=payload) + assert init_resp.status_code == 200 + agent_id = payload["agent"]["agent_id"] + + ctl_name = f"control-{uuid.uuid4()}" + ctl = client.put("/api/v1/controls", json={"name": ctl_name}) + assert ctl.status_code == 200 + control_id = ctl.json()["control_id"] + + from .utils import VALID_CONTROL_PAYLOAD + + set_data_resp = client.put( + f"/api/v1/controls/{control_id}/data", json={"data": VALID_CONTROL_PAYLOAD} + ) + assert set_data_resp.status_code == 200 + + # When: associating control directly with the agent + add_resp = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") + assert add_resp.status_code == 200 + assert add_resp.json()["success"] is True + + list_resp = client.get(f"/api/v1/agents/{agent_id}/controls") + assert list_resp.status_code == 200 + assert {item["id"] for item in list_resp.json()["controls"]} == {control_id} + + # When: removing direct control association from this agent + remove_resp = client.delete(f"/api/v1/agents/{agent_id}/controls/{control_id}") + assert remove_resp.status_code == 200 + assert remove_resp.json()["success"] is True + + # Then: agent no longer has the control + post_remove_list = client.get(f"/api/v1/agents/{agent_id}/controls") + assert post_remove_list.status_code == 200 + assert post_remove_list.json()["controls"] == [] + + # And: the control still exists globally + control_resp = client.get(f"/api/v1/controls/{control_id}") + assert control_resp.status_code == 200 + assert control_resp.json()["id"] == control_id + + def test_list_agent_controls_agent_not_found_404(client: TestClient) -> None: # Given: random agent id missing = str(uuid.uuid4()) diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-delete-control.ts index 78139ea2..28ca1367 100644 --- a/ui/src/core/hooks/query-hooks/use-delete-control.ts +++ b/ui/src/core/hooks/query-hooks/use-delete-control.ts @@ -3,30 +3,31 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/core/api/client'; import { parseApiError } from '@/core/api/errors'; -type DeleteControlParams = { +type RemoveControlFromAgentParams = { agentId: string; controlId: number; - /** If true, dissociate from all policy and agent links before deleting. */ - force?: boolean; }; /** - * Mutation hook to delete a control. - * Use force: true when deleting from agent detail so linked associations are removed first. + * Mutation hook to remove a control from a specific agent. */ -export function useDeleteControl() { +export function useRemoveControlFromAgent() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ controlId, force = true }: DeleteControlParams) => { - const { data, error, response } = await api.controls.delete(controlId, { - force, - }); + mutationFn: async ({ + agentId, + controlId, + }: RemoveControlFromAgentParams) => { + const { data, error, response } = await api.agents.removeControl( + agentId, + controlId + ); if (error) { throw parseApiError( error, - 'Failed to delete control', + 'Failed to remove control from agent', response?.status ); } 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 b991d7c3..6ab2ff46 100644 --- a/ui/src/core/page-components/agent-detail/agent-detail.tsx +++ b/ui/src/core/page-components/agent-detail/agent-detail.tsx @@ -73,7 +73,7 @@ const AgentDetailPage = ({ agentId, defaultTab }: AgentDetailPageProps) => { setSelectedControl(null); }; - const { handleDeleteControl, deleteControl } = useDeleteControlFlow({ + const { handleDeleteControl, removeControlFromAgent } = useDeleteControlFlow({ agentId, selectedControl, onCloseEditModal: handleCloseEditModal, @@ -137,7 +137,7 @@ const AgentDetailPage = ({ agentId, defaultTab }: AgentDetailPageProps) => { const columns = useControlsTableColumns({ agentId, updateControl, - deleteControl, + removeControlFromAgent, onEditControl: handleEditControl, onDeleteControl: handleDeleteControl, }); diff --git a/ui/src/core/page-components/agent-detail/controls/table-columns.tsx b/ui/src/core/page-components/agent-detail/controls/table-columns.tsx index 3eb438d7..21036415 100644 --- a/ui/src/core/page-components/agent-detail/controls/table-columns.tsx +++ b/ui/src/core/page-components/agent-detail/controls/table-columns.tsx @@ -6,7 +6,7 @@ import { type ColumnDef } from '@tanstack/react-table'; import { useMemo } from 'react'; import type { Control } from '@/core/api/types'; -import type { useDeleteControl } from '@/core/hooks/query-hooks/use-delete-control'; +import type { useRemoveControlFromAgent } from '@/core/hooks/query-hooks/use-delete-control'; import type { useUpdateControl } from '@/core/hooks/query-hooks/use-update-control'; import { getStepTypeLabelAndColor } from './utils'; @@ -14,7 +14,7 @@ import { getStepTypeLabelAndColor } from './utils'; type UseControlsTableColumnsParams = { agentId: string; updateControl: ReturnType; - deleteControl: ReturnType; + removeControlFromAgent: ReturnType; onEditControl: (control: Control) => void; onDeleteControl: (control: Control) => void; }; @@ -22,7 +22,7 @@ type UseControlsTableColumnsParams = { export function useControlsTableColumns({ agentId, updateControl, - deleteControl, + removeControlFromAgent, onEditControl, onDeleteControl, }: UseControlsTableColumnsParams): ColumnDef[] { @@ -190,8 +190,8 @@ export function useControlsTableColumns({ cell: ({ row }: { row: { original: Control } }) => { const control = row.original; const isDeleting = - deleteControl.isPending && - deleteControl.variables?.controlId === control.id; + removeControlFromAgent.isPending && + removeControlFromAgent.variables?.controlId === control.id; return ( onDeleteControl(control)} - aria-label="Delete control" + aria-label="Remove control from agent" disabled={isDeleting} > @@ -212,8 +212,8 @@ export function useControlsTableColumns({ [ agentId, updateControl, - deleteControl.isPending, - deleteControl.variables?.controlId, + removeControlFromAgent.isPending, + removeControlFromAgent.variables?.controlId, onEditControl, onDeleteControl, ] diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index d9123740..d05b473e 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -3,7 +3,7 @@ import { modals } from '@mantine/modals'; import { notifications } from '@mantine/notifications'; import type { Control } from '@/core/api/types'; -import { useDeleteControl } from '@/core/hooks/query-hooks/use-delete-control'; +import { useRemoveControlFromAgent } from '@/core/hooks/query-hooks/use-delete-control'; type UseDeleteControlFlowParams = { agentId: string; @@ -16,18 +16,18 @@ export function useDeleteControlFlow({ selectedControl, onCloseEditModal, }: UseDeleteControlFlowParams) { - const deleteControl = useDeleteControl(); + const removeControlFromAgent = useRemoveControlFromAgent(); const handleDeleteControl = (control: Control) => { modals.openConfirmModal({ - title: 'Delete control?', + title: 'Remove control from agent?', children: ( - Delete "{control.name}"? This will remove the control from - this agent and delete it. This action cannot be undone. + Remove "{control.name}" from this agent? This only removes + the association for this agent and does not delete the control globally. ), - labels: { confirm: 'Delete', cancel: 'Cancel' }, + labels: { confirm: 'Remove', cancel: 'Cancel' }, confirmProps: { variant: 'filled', color: 'red.7', @@ -35,17 +35,16 @@ export function useDeleteControlFlow({ }, cancelProps: { variant: 'default', size: 'sm' }, onConfirm: () => - deleteControl.mutate( + removeControlFromAgent.mutate( { agentId, controlId: control.id, - force: true, }, { onSuccess: () => { notifications.show({ - title: 'Control deleted', - message: `"${control.name}" has been removed.`, + title: 'Control removed', + message: `"${control.name}" has been removed from this agent.`, color: 'green', }); if (selectedControl?.id === control.id) { @@ -54,7 +53,7 @@ export function useDeleteControlFlow({ }, onError: (error) => { notifications.show({ - title: 'Failed to delete control', + title: 'Failed to remove control', message: error instanceof Error ? error.message @@ -67,5 +66,5 @@ export function useDeleteControlFlow({ }); }; - return { handleDeleteControl, deleteControl }; + return { handleDeleteControl, removeControlFromAgent }; } From c68872afb60d17ad0a34c416468d7baf1402c4d1 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 14:32:39 -0500 Subject: [PATCH 03/32] feat: expose control usage counts and refresh docs --- models/src/agent_control_models/server.py | 4 ++ .../src/generated/models/control-summary.ts | 6 ++ server/README.md | 47 +++++++++----- .../agent_control_server/endpoints/agents.py | 2 +- .../endpoints/controls.py | 17 +++-- server/tests/test_controls_additional.py | 65 +++++++++++++++++-- ui/src/core/api/generated/api-types.ts | 6 ++ .../modals/control-store/index.tsx | 37 ++--------- ui/tests/fixtures.ts | 7 +- 9 files changed, 129 insertions(+), 62 deletions(-) diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index 6be72aa9..f38a86a8 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -273,6 +273,10 @@ class ControlSummary(BaseModel): stages: list[str] | None = Field(None, description="Evaluation stages in scope") tags: list[str] = Field(default_factory=list, description="Control tags") used_by_agent: AgentRef | None = Field(None, description="Agent using this control") + # TODO: Follow-up with full `used_by_agents` list for richer attribution. + used_by_agents_count: int = Field( + 0, description="Number of unique agents using this control" + ) class ListControlsResponse(BaseModel): diff --git a/sdks/typescript/src/generated/models/control-summary.ts b/sdks/typescript/src/generated/models/control-summary.ts index 9e0356a5..7e89a07f 100644 --- a/sdks/typescript/src/generated/models/control-summary.ts +++ b/sdks/typescript/src/generated/models/control-summary.ts @@ -50,6 +50,10 @@ export type ControlSummary = { * Agent using this control */ usedByAgent?: AgentRef | null | undefined; + /** + * Number of unique agents using this control + */ + usedByAgentsCount: number; }; /** @internal */ @@ -67,11 +71,13 @@ export const ControlSummary$inboundSchema: z.ZodMiniType< step_types: z.optional(z.nullable(z.array(types.string()))), tags: types.optional(z.array(types.string())), used_by_agent: z.optional(z.nullable(AgentRef$inboundSchema)), + used_by_agents_count: z._default(types.number(), 0), }), z.transform((v) => { return remap$(v, { "step_types": "stepTypes", "used_by_agent": "usedByAgent", + "used_by_agents_count": "usedByAgentsCount", }); }), ); diff --git a/server/README.md b/server/README.md index 7af8ec01..09d0cba5 100644 --- a/server/README.md +++ b/server/README.md @@ -155,32 +155,45 @@ GET /api/v1/evaluators ```bash # Register or update agent -POST /api/v1/agents/init -Body: { "agent": {...}, "tools": [...], "force_replace": false } +POST /api/v1/agents/initAgent +Body: { "agent": {...}, "steps": [...], "evaluators": [...], "force_replace": false } # Get agent GET /api/v1/agents/{agent_id} -# List controls for agent (based on assigned policy) +# List active controls for agent (union of policy-derived + direct agent controls) GET /api/v1/agents/{agent_id}/controls + +# Add/remove policy associations on agent (many-to-many) +POST /api/v1/agents/{agent_id}/policies/{policy_id} +GET /api/v1/agents/{agent_id}/policies +DELETE /api/v1/agents/{agent_id}/policies/{policy_id} +DELETE /api/v1/agents/{agent_id}/policies + +# Add/remove direct control associations on agent (many-to-many) +POST /api/v1/agents/{agent_id}/controls/{control_id} +DELETE /api/v1/agents/{agent_id}/controls/{control_id} ``` ### Control Management ```bash # Create control -POST /api/v1/controls -Body: { "control": {...} } +PUT /api/v1/controls +Body: { "name": "my-control" } # List controls -GET /api/v1/controls?skip=0&limit=100 +GET /api/v1/controls?limit=100 # Get control GET /api/v1/controls/{control_id} -# Update control -PUT /api/v1/controls/{control_id} -Body: { "control": {...} } +# Update control metadata +PATCH /api/v1/controls/{control_id} + +# Set control data +PUT /api/v1/controls/{control_id}/data +Body: { "data": {...} } # Delete control DELETE /api/v1/controls/{control_id} @@ -190,17 +203,17 @@ DELETE /api/v1/controls/{control_id} ```bash # Create policy -POST /api/v1/policies -Body: { "name": "my-policy", "description": "..." } - -# List policies -GET /api/v1/policies - -# Assign policy to agent -POST /api/v1/policies/{policy_id}/agents/{agent_id} +PUT /api/v1/policies +Body: { "name": "my-policy" } # Add control to policy POST /api/v1/policies/{policy_id}/controls/{control_id} + +# Remove control from policy +DELETE /api/v1/policies/{policy_id}/controls/{control_id} + +# List policy controls +GET /api/v1/policies/{policy_id}/controls ``` ### Evaluation diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index c074e759..42e75225 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -352,7 +352,7 @@ async def init_agent( db: Database session (injected) Returns: - InitAgentResponse with created flag and active controls (if policy assigned) + InitAgentResponse with created flag and active controls Raises: HTTPException 409: Agent name exists with different UUID diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index 0cf96497..48a39973 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -593,11 +593,13 @@ async def list_controls( if has_more: controls = controls[:-1] - # Build mapping of control_id -> agent that uses it + # Build mapping of control_id -> usage attribution # Traversal includes both: # - Control -> policy_controls -> agent_policies -> Agent # - Control -> agent_controls -> Agent control_agent_map: dict[int, AgentRef | None] = {ctrl.id: None for ctrl in controls} + control_agent_ids_map: dict[int, set[str]] = {ctrl.id: set() for ctrl in controls} + control_agent_repr_map: dict[int, tuple[str, str] | None] = {ctrl.id: None for ctrl in controls} if controls: control_ids = [ctrl.id for ctrl in controls] policy_agents_query = ( @@ -625,10 +627,16 @@ async def list_controls( agents_result = await db.execute(agents_query) for row in agents_result.all(): control_id, agent_uuid, agent_name = row - # Take the first agent found (1 control = 1 agent) - if control_agent_map[control_id] is None: + agent_uuid_str = str(agent_uuid) + control_agent_ids_map[control_id].add(agent_uuid_str) + + # Keep a deterministic representative agent for backward compatibility. + current_repr = control_agent_repr_map[control_id] + candidate_repr = (agent_name, agent_uuid_str) + if current_repr is None or candidate_repr < current_repr: + control_agent_repr_map[control_id] = candidate_repr control_agent_map[control_id] = AgentRef( - agent_id=str(agent_uuid), agent_name=agent_name + agent_id=agent_uuid_str, agent_name=agent_name ) # Build summaries (filtering already done at DB level) @@ -648,6 +656,7 @@ async def list_controls( stages=scope.get("stages"), tags=data.get("tags", []), used_by_agent=control_agent_map.get(ctrl.id), + used_by_agents_count=len(control_agent_ids_map.get(ctrl.id, set())), ) ) diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 1662d3b1..dc2fed69 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -6,15 +6,12 @@ from types import SimpleNamespace import pytest -from fastapi.testclient import TestClient -from sqlalchemy import text -from sqlalchemy.orm import Session - -from agent_control_server.models import Control - from agent_control_evaluators import RegexEvaluatorConfig from agent_control_server.endpoints import controls as controls_module from agent_control_server.models import Control +from fastapi.testclient import TestClient +from sqlalchemy import text +from sqlalchemy.orm import Session from .conftest import engine from .utils import VALID_CONTROL_PAYLOAD @@ -32,6 +29,24 @@ def _set_control_data(client: TestClient, control_id: int, data: dict) -> None: assert resp.status_code == 200, resp.text +def _init_agent(client: TestClient, name: str | None = None) -> str: + agent_id = str(uuid.uuid4()) + payload = { + "agent": { + "agent_id": agent_id, + "agent_name": name or f"agent-{uuid.uuid4()}", + "agent_description": "test", + "agent_version": "1.0", + "agent_metadata": {}, + }, + "steps": [], + "evaluators": [], + } + resp = client.post("/api/v1/agents/initAgent", json=payload) + assert resp.status_code == 200 + return agent_id + + def test_list_controls_filters_and_pagination(client: TestClient) -> None: # Given: three controls with varying data control1_id, control1_name = _create_control(client, name=f"AlphaControl-{uuid.uuid4()}") @@ -126,6 +141,40 @@ def test_list_controls_filters_and_pagination(client: TestClient) -> None: assert page2["controls"][0]["id"] != first_id +def test_list_controls_includes_unique_used_by_agents_count(client: TestClient) -> None: + # Given: one control linked to two agents via policy and one duplicated direct link + control_id, control_name = _create_control(client, name=f"CountedControl-{uuid.uuid4()}") + _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) + + policy_resp = client.put("/api/v1/policies", json={"name": f"policy-{uuid.uuid4()}"}) + assert policy_resp.status_code == 200 + policy_id = policy_resp.json()["policy_id"] + + assoc_control = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") + assert assoc_control.status_code == 200 + + agent_a_id = _init_agent(client, name=f"agent-a-{uuid.uuid4()}") + agent_b_id = _init_agent(client, name=f"agent-b-{uuid.uuid4()}") + + assoc_policy_a = client.post(f"/api/v1/agents/{agent_a_id}/policies/{policy_id}") + assoc_policy_b = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") + assert assoc_policy_a.status_code == 200 + assert assoc_policy_b.status_code == 200 + + # Duplicate path for agent A: direct + policy should still count as one unique agent + assoc_direct_a = client.post(f"/api/v1/agents/{agent_a_id}/controls/{control_id}") + assert assoc_direct_a.status_code == 200 + + # When: listing controls + list_resp = client.get("/api/v1/controls", params={"name": control_name}) + assert list_resp.status_code == 200 + controls = list_resp.json()["controls"] + assert len(controls) == 1 + + # Then: used-by count is de-duplicated at agent level + assert controls[0]["used_by_agents_count"] == 2 + + def test_patch_control_enabled_requires_data(client: TestClient) -> None: # Given: a control without configured data control_id, _ = _create_control(client) @@ -239,7 +288,9 @@ def test_list_controls_enabled_true_includes_missing_enabled(client: TestClient) # Given: controls with enabled true, enabled false, and missing enabled control_true_id, control_true_name = _create_control(client, name=f"Enabled-{uuid.uuid4()}") control_false_id, control_false_name = _create_control(client, name=f"Disabled-{uuid.uuid4()}") - control_missing_id, control_missing_name = _create_control(client, name=f"Missing-{uuid.uuid4()}") + control_missing_id, control_missing_name = _create_control( + client, name=f"Missing-{uuid.uuid4()}" + ) data_true = deepcopy(VALID_CONTROL_PAYLOAD) data_true["enabled"] = True diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index 3a57b1b2..14c5daa9 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -1629,6 +1629,12 @@ export interface components { tags?: string[]; /** @description Agent using this control */ used_by_agent?: components["schemas"]["AgentRef"] | null; + /** + * Used By Agents Count + * @description Number of unique agents using this control + * @default 0 + */ + used_by_agents_count: number; }; /** CreateControlRequest */ CreateControlRequest: { 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..175a68f3 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 @@ -1,5 +1,4 @@ import { - Anchor, Box, Divider, Group, @@ -16,16 +15,11 @@ import { notifications } from '@mantine/notifications'; import { Button, Table } from '@rungalileo/jupiter-ds'; import { IconAlertCircle, IconX } from '@tabler/icons-react'; import { type ColumnDef } from '@tanstack/react-table'; -import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { ErrorBoundary } from '@/components/error-boundary'; import { api } from '@/core/api/client'; -import type { - AgentRef, - ControlDefinition, - ControlSummary, -} from '@/core/api/types'; +import type { ControlDefinition, ControlSummary } from '@/core/api/types'; import { SearchInput } from '@/core/components/search-input'; import { MODAL_NAMES, SUBMODAL_NAMES } from '@/core/constants/modal-routes'; import { useControlsInfinite } from '@/core/hooks/query-hooks/use-controls-infinite'; @@ -37,11 +31,6 @@ 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) -type ControlSummaryWithAgent = ControlSummary & { - used_by_agent?: AgentRef | null; -}; - type ControlStoreModalProps = { opened: boolean; onClose: () => void; @@ -276,33 +265,21 @@ export function ControlStoreModal({ }, { id: 'agent', - header: 'Agent', + header: 'Used by', size: 150, cell: ({ row }) => { - const agent = (row.original as ControlSummaryWithAgent).used_by_agent; - const control = row.original; - if (!agent) { + const count = row.original.used_by_agents_count ?? 0; + if (count === 0) { return ( ); } - // Link to agent controls tab with control name filter - const href = `/agents/${agent.agent_id}/controls?q=${encodeURIComponent(control.name)}`; return ( - { - e.stopPropagation(); - // Close modal when navigating to agent page - onClose(); - }} - > - {agent.agent_name} - + + {count} {count === 1 ? 'agent' : 'agents'} + ); }, }, diff --git a/ui/tests/fixtures.ts b/ui/tests/fixtures.ts index 942a50d6..5cc1d644 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -147,9 +147,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; -})[] = [ +const controlSummariesList: ControlSummary[] = [ { id: 1, name: 'PII Detection', @@ -160,6 +158,7 @@ const controlSummariesList: (ControlSummary & { stages: ['post'], tags: ['pii', 'compliance'], used_by_agent: { agent_id: 'agent-1', agent_name: 'Customer Support Bot' }, + used_by_agents_count: 1, }, { id: 2, @@ -171,6 +170,7 @@ const controlSummariesList: (ControlSummary & { stages: ['pre'], tags: ['security'], used_by_agent: { agent_id: 'agent-2', agent_name: 'Data Analysis Agent' }, + used_by_agents_count: 1, }, { id: 3, @@ -182,6 +182,7 @@ const controlSummariesList: (ControlSummary & { stages: ['pre'], tags: [], used_by_agent: null, + used_by_agents_count: 0, }, ]; From 08757f99d1e10c180920de11c6f050d830d188e5 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 15:21:25 -0500 Subject: [PATCH 04/32] fix: clarify agent control removal and cleanup failed add flow --- models/src/agent_control_models/server.py | 12 ++ .../agent_control_server/endpoints/agents.py | 44 ++++++- server/tests/test_init_agent.py | 49 ++++++++ .../query-hooks/use-add-control-to-agent.ts | 84 +++++++------ .../hooks/query-hooks/use-delete-control.ts | 11 +- .../controls/use-delete-control-flow.tsx | 28 ++++- ui/tests/control-store.spec.ts | 113 ++++++++++++++++++ 7 files changed, 294 insertions(+), 47 deletions(-) diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index f38a86a8..10f81724 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -157,6 +157,18 @@ class AssocResponse(BaseModel): success: bool = Field(description="Whether the association change succeeded") +class RemoveAgentControlResponse(BaseModel): + """Response for removing a direct agent-control association.""" + + success: bool = Field(description="Whether the request succeeded") + removed_direct_association: bool = Field( + description="True if a direct agent-control link was removed" + ) + control_still_active: bool = Field( + description="True if the control remains active via policy association(s)" + ) + + class GetControlDataResponse(BaseModel): data: ControlDefinition = Field(description="Control data payload") diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 42e75225..fb9f4539 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -18,6 +18,7 @@ PaginationInfo, PatchAgentRequest, PatchAgentResponse, + RemoveAgentControlResponse, StepKey, ) from fastapi import APIRouter, Depends @@ -990,13 +991,13 @@ async def add_agent_control( @router.delete( "/{agent_id}/controls/{control_id}", - response_model=AssocResponse, + response_model=RemoveAgentControlResponse, summary="Remove direct control association from agent", response_description="Success confirmation", ) async def remove_agent_control( agent_id: UUID, control_id: int, db: AsyncSession = Depends(get_async_db) -) -> AssocResponse: +) -> RemoveAgentControlResponse: """Remove a direct control association from an agent (idempotent).""" await _get_agent_or_404(agent_id, db) @@ -1011,12 +1012,41 @@ async def remove_agent_control( ) try: - await db.execute( - delete(agent_controls).where( + direct_assoc_result = await db.execute( + select(agent_controls.c.control_id) + .where( (agent_controls.c.agent_uuid == agent_id) & (agent_controls.c.control_id == control_id) ) + .limit(1) ) + removed_direct_association = direct_assoc_result.first() is not None + + if removed_direct_association: + await db.execute( + delete(agent_controls).where( + (agent_controls.c.agent_uuid == agent_id) + & (agent_controls.c.control_id == control_id) + ) + ) + + # The control may still be active for this agent if inherited from policy association(s). + policy_inheritance_result = await db.execute( + select(policy_controls.c.control_id) + .select_from( + agent_policies.join( + policy_controls, + agent_policies.c.policy_id == policy_controls.c.policy_id, + ) + ) + .where( + (agent_policies.c.agent_uuid == agent_id) + & (policy_controls.c.control_id == control_id) + ) + .limit(1) + ) + control_still_active = policy_inheritance_result.first() is not None + await db.commit() except Exception: await db.rollback() @@ -1032,7 +1062,11 @@ async def remove_agent_control( operation="remove control association", ) - return AssocResponse(success=True) + return RemoveAgentControlResponse( + success=True, + removed_direct_association=removed_direct_association, + control_still_active=control_still_active, + ) @router.get( diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index 102b31f1..e03e35b5 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -456,6 +456,8 @@ def test_add_and_remove_direct_agent_control_does_not_delete_global_control( remove_resp = client.delete(f"/api/v1/agents/{agent_id}/controls/{control_id}") assert remove_resp.status_code == 200 assert remove_resp.json()["success"] is True + assert remove_resp.json()["removed_direct_association"] is True + assert remove_resp.json()["control_still_active"] is False # Then: agent no longer has the control post_remove_list = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -468,6 +470,53 @@ def test_add_and_remove_direct_agent_control_does_not_delete_global_control( assert control_resp.json()["id"] == control_id +def test_remove_policy_derived_control_reports_no_direct_association_removed( + client: TestClient, +) -> None: + # Given: an agent inheriting a control from a policy (no direct association) + payload = make_agent_payload() + init_resp = client.post("/api/v1/agents/initAgent", json=payload) + assert init_resp.status_code == 200 + agent_id = payload["agent"]["agent_id"] + + policy_name = f"policy-{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"] + + control_name = f"control-{uuid.uuid4()}" + control_resp = client.put("/api/v1/controls", json={"name": control_name}) + assert control_resp.status_code == 200 + control_id = control_resp.json()["control_id"] + + from .utils import VALID_CONTROL_PAYLOAD + + set_data_resp = client.put( + f"/api/v1/controls/{control_id}/data", + json={"data": VALID_CONTROL_PAYLOAD}, + ) + assert set_data_resp.status_code == 200 + + assoc_control_resp = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") + assert assoc_control_resp.status_code == 200 + assoc_policy_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assert assoc_policy_resp.status_code == 200 + + # When: removing via direct-control endpoint + remove_resp = client.delete(f"/api/v1/agents/{agent_id}/controls/{control_id}") + + # Then: request succeeds but reports that nothing direct was removed + assert remove_resp.status_code == 200 + assert remove_resp.json()["success"] is True + assert remove_resp.json()["removed_direct_association"] is False + assert remove_resp.json()["control_still_active"] is True + + # And: control remains active due to policy inheritance + list_resp = client.get(f"/api/v1/agents/{agent_id}/controls") + assert list_resp.status_code == 200 + assert {item["id"] for item in list_resp.json()["controls"]} == {control_id} + + def test_list_agent_controls_agent_not_found_404(client: TestClient) -> None: # Given: random agent id missing = str(uuid.uuid4()) diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index 9992aba2..a7a619dd 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -26,50 +26,60 @@ export function useAddControlToAgent() { controlName, definition, }: AddControlToAgentParams) => { - // Step 1: Create the control - const { - data: createControlResult, - error: createControlError, - response: createControlResponse, - } = await api.controls.create({ name: controlName }); + let createdControlId: number | null = null; - if (createControlError || !createControlResult) { - throw parseApiError( - createControlError, - 'Failed to create control', - createControlResponse?.status - ); - } + try { + // Step 1: Create the control + const { + data: createControlResult, + error: createControlError, + response: createControlResponse, + } = await api.controls.create({ name: controlName }); - const controlId = createControlResult.control_id; + if (createControlError || !createControlResult) { + throw parseApiError( + createControlError, + 'Failed to create control', + createControlResponse?.status + ); + } - // Step 2: Set control data (definition) - const { error: setDataError, response: setDataResponse } = - await api.controls.setData(controlId, { - data: definition, - }); + createdControlId = createControlResult.control_id; - if (setDataError) { - throw parseApiError( - setDataError, - 'Failed to set control data', - setDataResponse?.status - ); - } + // Step 2: Set control data (definition) + const { error: setDataError, response: setDataResponse } = + await api.controls.setData(createdControlId, { + data: definition, + }); - // Step 3: Associate control directly with the agent - const { error: associateError, response: associateResponse } = - await api.agents.addControl(agentId, controlId); + if (setDataError) { + throw parseApiError( + setDataError, + 'Failed to set control data', + setDataResponse?.status + ); + } - if (associateError) { - throw parseApiError( - associateError, - 'Failed to associate control with agent', - associateResponse?.status - ); - } + // Step 3: Associate control directly with the agent + const { error: associateError, response: associateResponse } = + await api.agents.addControl(agentId, createdControlId); - return { controlId }; + if (associateError) { + throw parseApiError( + associateError, + 'Failed to associate control with agent', + associateResponse?.status + ); + } + + return { controlId: createdControlId }; + } catch (error) { + // Best effort cleanup: avoid orphan controls if a later step fails. + if (createdControlId !== null) { + await api.controls.delete(createdControlId, { force: true }); + } + throw error; + } }, onSuccess: (_data, variables) => { // Invalidate relevant queries to refetch data diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-delete-control.ts index 28ca1367..8cfe3c20 100644 --- a/ui/src/core/hooks/query-hooks/use-delete-control.ts +++ b/ui/src/core/hooks/query-hooks/use-delete-control.ts @@ -8,6 +8,12 @@ type RemoveControlFromAgentParams = { controlId: number; }; +export type RemoveControlFromAgentResult = { + success: boolean; + removed_direct_association?: boolean; + control_still_active?: boolean; +}; + /** * Mutation hook to remove a control from a specific agent. */ @@ -32,12 +38,15 @@ export function useRemoveControlFromAgent() { ); } - return data; + return (data ?? { success: true }) as RemoveControlFromAgentResult; }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ['agent', variables.agentId, 'controls'], }); + queryClient.invalidateQueries({ + queryKey: ['controls', 'infinite'], + }); queryClient.invalidateQueries({ queryKey: ['agents', 'infinite'], }); diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index d05b473e..2a1e32a2 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -3,7 +3,10 @@ import { modals } from '@mantine/modals'; import { notifications } from '@mantine/notifications'; import type { Control } from '@/core/api/types'; -import { useRemoveControlFromAgent } from '@/core/hooks/query-hooks/use-delete-control'; +import { + type RemoveControlFromAgentResult, + useRemoveControlFromAgent, +} from '@/core/hooks/query-hooks/use-delete-control'; type UseDeleteControlFlowParams = { agentId: string; @@ -41,10 +44,27 @@ export function useDeleteControlFlow({ controlId: control.id, }, { - onSuccess: () => { + onSuccess: (result: RemoveControlFromAgentResult) => { + const removedDirect = + result.removed_direct_association ?? true; + const stillActive = result.control_still_active ?? false; + + if (!removedDirect) { + notifications.show({ + title: 'Control inherited from policy', + message: `"${control.name}" has no direct link on this agent. Remove it from policy to disable it.`, + color: 'yellow', + }); + return; + } + notifications.show({ - title: 'Control removed', - message: `"${control.name}" has been removed from this agent.`, + title: stillActive + ? 'Direct association removed' + : 'Control removed', + message: stillActive + ? `"${control.name}" is still active through policy inheritance.` + : `"${control.name}" has been removed from this agent.`, color: 'green', }); if (selectedControl?.id === control.id) { diff --git a/ui/tests/control-store.spec.ts b/ui/tests/control-store.spec.ts index 17f95204..9d696a6c 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -659,6 +659,119 @@ test.describe('Modal Routing', () => { // URL should not contain any modal parameters await expect(mockedPage).not.toHaveURL(/.*\?modal=/); }); + + test('cleans up created control when agent association fails', async ({ + mockedPage, + }) => { + await mockedPage.goto( + `${agentUrl}?modal=control-store&submodal=create&evaluator=list` + ); + + const createModal = mockedPage.getByRole('dialog', { + name: 'Create Control', + }); + await expect(createModal).toBeVisible(); + + let cleanupDeleteCalls = 0; + + await mockedPage.route( + '**/api/v1/agents/*/controls/*', + async (route, request) => { + if (request.method() === 'POST') { + await route.fulfill({ + status: 400, + contentType: 'application/json', + body: JSON.stringify({ + detail: 'Control is incompatible with this agent', + error_code: 'POLICY_CONTROL_INCOMPATIBLE', + }), + }); + } else { + await route.continue(); + } + } + ); + + await mockedPage.route('**/api/v1/controls', async (route, request) => { + if (request.method() === 'PUT') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ control_id: 100 }), + }); + } else { + await route.continue(); + } + }); + + await mockedPage.route( + '**/api/v1/controls/*/data', + async (route, request) => { + if (request.method() === 'PUT') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true }), + }); + } else { + await route.continue(); + } + } + ); + + await mockedPage.route('**/api/v1/controls/*', async (route, request) => { + if (request.method() === 'DELETE') { + cleanupDeleteCalls += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + dissociated_from_policies: [], + dissociated_from_agents: [], + }), + }); + } else { + await route.continue(); + } + }); + + const controlNameInput = createModal.getByPlaceholder('Enter control name'); + await controlNameInput.fill('Cleanup Test Control'); + const valuesTextarea = createModal.getByPlaceholder( + 'Enter values (one per line)' + ); + await valuesTextarea.fill('value-1'); + + const saveButton = createModal.getByRole('button', { + name: /Save|Create/i, + }); + await saveButton.click(); + + const confirmButton = mockedPage.locator("button:has-text('Confirm')"); + await expect(confirmButton).toBeVisible({ timeout: 5000 }); + + const failedAssociationPromise = mockedPage.waitForResponse( + (response) => + response.request().method() === 'POST' && + /\/api\/v1\/agents\/[^/]+\/controls\/\d+/.test(response.url()) && + response.status() === 400, + { timeout: 10000 } + ); + const cleanupDeletePromise = mockedPage.waitForResponse( + (response) => + response.request().method() === 'DELETE' && + /\/api\/v1\/controls\/\d+\?force=true/.test(response.url()) && + response.status() === 200, + { timeout: 10000 } + ); + + await confirmButton.click(); + await failedAssociationPromise; + await cleanupDeletePromise; + + expect(cleanupDeleteCalls).toBe(1); + }); }); test.describe('Control Store - Loading States', () => { From ae45dc2b6b490a74feb04e8e12cf9a39b5e8d3fa Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 15:30:15 -0500 Subject: [PATCH 05/32] chore: regenerate ts sdk and ui api types --- .../src/generated/funcs/agents-init.ts | 2 +- .../generated/funcs/agents-remove-control.ts | 8 +-- sdks/typescript/src/generated/models/index.ts | 1 + .../models/remove-agent-control-response.ts | 56 +++++++++++++++++++ sdks/typescript/src/generated/sdk/agents.ts | 4 +- ui/src/core/api/generated/api-types.ts | 25 ++++++++- 6 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 sdks/typescript/src/generated/models/remove-agent-control-response.ts diff --git a/sdks/typescript/src/generated/funcs/agents-init.ts b/sdks/typescript/src/generated/funcs/agents-init.ts index f713abf2..f056db7d 100644 --- a/sdks/typescript/src/generated/funcs/agents-init.ts +++ b/sdks/typescript/src/generated/funcs/agents-init.ts @@ -46,7 +46,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) + * InitAgentResponse with created flag and active controls * * Raises: * HTTPException 409: Agent name exists with different UUID diff --git a/sdks/typescript/src/generated/funcs/agents-remove-control.ts b/sdks/typescript/src/generated/funcs/agents-remove-control.ts index 7005fd6e..9c8dadc3 100644 --- a/sdks/typescript/src/generated/funcs/agents-remove-control.ts +++ b/sdks/typescript/src/generated/funcs/agents-remove-control.ts @@ -40,7 +40,7 @@ export function agentsRemoveControl( options?: RequestOptions, ): APIPromise< Result< - models.AssocResponse, + models.RemoveAgentControlResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -67,7 +67,7 @@ async function $do( ): Promise< [ Result< - models.AssocResponse, + models.RemoveAgentControlResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -167,7 +167,7 @@ async function $do( }; const [result] = await M.match< - models.AssocResponse, + models.RemoveAgentControlResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -178,7 +178,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.AssocResponse$inboundSchema), + M.json(200, models.RemoveAgentControlResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), diff --git a/sdks/typescript/src/generated/models/index.ts b/sdks/typescript/src/generated/models/index.ts index 4582184f..fc1a8e92 100644 --- a/sdks/typescript/src/generated/models/index.ts +++ b/sdks/typescript/src/generated/models/index.ts @@ -53,6 +53,7 @@ export * from "./patch-agent-request.js"; export * from "./patch-agent-response.js"; export * from "./patch-control-request.js"; export * from "./patch-control-response.js"; +export * from "./remove-agent-control-response.js"; export * from "./security.js"; export * from "./set-control-data-request.js"; export * from "./set-control-data-response.js"; diff --git a/sdks/typescript/src/generated/models/remove-agent-control-response.ts b/sdks/typescript/src/generated/models/remove-agent-control-response.ts new file mode 100644 index 00000000..e0c61fde --- /dev/null +++ b/sdks/typescript/src/generated/models/remove-agent-control-response.ts @@ -0,0 +1,56 @@ +/* + * 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 { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import * as types from "../types/primitives.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; + +/** + * Response for removing a direct agent-control association. + */ +export type RemoveAgentControlResponse = { + /** + * True if the control remains active via policy association(s) + */ + controlStillActive: boolean; + /** + * True if a direct agent-control link was removed + */ + removedDirectAssociation: boolean; + /** + * Whether the request succeeded + */ + success: boolean; +}; + +/** @internal */ +export const RemoveAgentControlResponse$inboundSchema: z.ZodMiniType< + RemoveAgentControlResponse, + unknown +> = z.pipe( + z.object({ + control_still_active: types.boolean(), + removed_direct_association: types.boolean(), + success: types.boolean(), + }), + z.transform((v) => { + return remap$(v, { + "control_still_active": "controlStillActive", + "removed_direct_association": "removedDirectAssociation", + }); + }), +); + +export function removeAgentControlResponseFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => RemoveAgentControlResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'RemoveAgentControlResponse' from JSON`, + ); +} diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index b1e0d014..ad1dfff3 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -70,7 +70,7 @@ export class Agents extends ClientSDK { * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) + * InitAgentResponse with created flag and active controls * * Raises: * HTTPException 409: Agent name exists with different UUID @@ -188,7 +188,7 @@ export class Agents extends ClientSDK { request: operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, options?: RequestOptions, - ): Promise { + ): Promise { return unwrapAsync(agentsRemoveControl( this, request, diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index 14c5daa9..5a80b68d 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -63,7 +63,7 @@ export interface paths { * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) + * InitAgentResponse with created flag and active controls * * Raises: * HTTPException 409: Agent name exists with different UUID @@ -2500,6 +2500,27 @@ export interface components { */ success: boolean; }; + /** + * RemoveAgentControlResponse + * @description Response for removing a direct agent-control association. + */ + RemoveAgentControlResponse: { + /** + * Control Still Active + * @description True if the control remains active via policy association(s) + */ + control_still_active: boolean; + /** + * Removed Direct Association + * @description True if a direct agent-control link was removed + */ + removed_direct_association: boolean; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; /** * SetControlDataRequest * @description Request to update control configuration data. @@ -3059,7 +3080,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["AssocResponse"]; + "application/json": components["schemas"]["RemoveAgentControlResponse"]; }; }; /** @description Validation Error */ From e10636b156f9f5ae4c0bc902e54f54ef49c718a7 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 15:52:48 -0500 Subject: [PATCH 06/32] chore(ui): apply prettier formatting --- ui/src/core/api/client.ts | 5 +- ui/src/core/api/generated/api-types.ts | 8176 +++++++++-------- .../controls/use-delete-control-flow.tsx | 6 +- 3 files changed, 4104 insertions(+), 4083 deletions(-) diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index a071fafe..9213c4af 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -85,7 +85,10 @@ export const api = { apiClient.POST('/api/v1/agents/{agent_id}/controls/{control_id}', { params: { path: { agent_id: agentId, control_id: controlId } }, }), - removeControl: (agentId: GetAgentPathParams['agent_id'], controlId: number) => + removeControl: ( + agentId: GetAgentPathParams['agent_id'], + controlId: number + ) => apiClient.DELETE('/api/v1/agents/{agent_id}/controls/{control_id}', { params: { path: { agent_id: agentId, control_id: controlId } }, }), diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index 5a80b68d..d4bfc948 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -4,4092 +4,4110 @@ */ export interface paths { - "/api/v1/agents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all agents - * @description List all registered agents with cursor-based pagination. - * - * Returns a summary of each agent including ID, name, policy associations, - * and counts of registered steps and evaluators. - * - * Args: - * cursor: Optional cursor for pagination (UUID of last agent from previous page) - * limit: Pagination limit (default 20, max 100) - * name: Optional name filter (case-insensitive partial match) - * db: Database session (injected) - * - * Returns: - * ListAgentsResponse with agent summaries and pagination info - */ - get: operations["list_agents_api_v1_agents_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/initAgent": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Initialize or update an agent - * @description Register a new agent or update an existing agent's steps and metadata. - * - * 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 UUID exists with a different name, returns 409 Conflict (no renames) - * - * Step versioning: When step schemas change (input_schema or output_schema), - * a new version is created automatically. - * - * Args: - * request: Agent metadata and step schemas - * db: Database session (injected) - * - * Returns: - * InitAgentResponse with created flag and active controls - * - * 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; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent details - * @description 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 - * db: Database session (injected) - * - * Returns: - * GetAgentResponse with agent metadata and step list - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 422: Agent data is corrupted - */ - get: operations["get_agent_api_v1_agents__agent_id__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify agent (remove steps/evaluators) - * @description Remove steps and/or evaluators from an agent. - * - * This is the complement to initAgent which only adds items. - * Removals are idempotent - attempting to remove non-existent items is not an error. - * - * Args: - * agent_id: UUID of the agent - * request: Lists of step/evaluator identifiers to remove - * db: Database session (injected) - * - * Returns: - * PatchAgentResponse with lists of actually removed items - * - * Raises: - * 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}/controls": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's active controls - * @description List all protection controls active for an agent. - * - * Controls include the union of policy-derived and directly associated controls. - * - * Args: - * agent_id: UUID of the agent - * db: Database session (injected) - * - * Returns: - * AgentControlsResponse with list of active controls - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations["list_agent_controls_api_v1_agents__agent_id__controls_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}/controls/{control_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate control directly with agent - * @description Associate a control directly with an agent (idempotent). - */ - post: operations["add_agent_control_api_v1_agents__agent_id__controls__control_id__post"]; - /** - * Remove direct control association from agent - * @description Remove a direct control association from an agent (idempotent). - */ - delete: operations["remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}/evaluators": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's registered evaluator schemas - * @description List all evaluator schemas registered with an agent. - * - * Evaluator schemas are registered via initAgent and used for: - * - Config validation when creating Controls - * - UI to display available config options - * - * Args: - * agent_id: UUID of the agent - * cursor: Optional cursor for pagination (name of last evaluator from previous page) - * limit: Pagination limit (default 20, max 100) - * db: Database session (injected) - * - * Returns: - * ListEvaluatorsResponse with evaluator schemas and pagination - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations["list_agent_evaluators_api_v1_agents__agent_id__evaluators_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}/evaluators/{evaluator_name}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get specific evaluator schema - * @description Get a specific evaluator schema registered with an agent. - * - * Args: - * agent_id: UUID of the agent - * evaluator_name: Name of the evaluator - * db: Database session (injected) - * - * Returns: - * EvaluatorSchemaItem with schema details - * - * Raises: - * HTTPException 404: Agent or evaluator not found - */ - get: operations["get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}/policies": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List policies associated with agent - * @description List policy IDs associated with an agent. - */ - get: operations["get_agent_policies_api_v1_agents__agent_id__policies_get"]; - put?: never; - post?: never; - /** - * Remove all policy associations from agent - * @description Remove all policy associations from an agent. - */ - delete: operations["remove_all_agent_policies_api_v1_agents__agent_id__policies_delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_id}/policies/{policy_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate policy with agent - * @description Associate a policy with an agent (idempotent). - */ - post: operations["add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post"]; - /** - * Remove policy association from agent - * @description Remove a policy association from an agent (idempotent). - */ - delete: operations["remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/controls": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all controls - * @description List all controls with optional filtering and cursor-based pagination. - * - * Controls are returned ordered by ID descending (newest first). - * - * Args: - * cursor: ID of the last control from the previous page (for pagination) - * limit: Maximum number of controls to return (default 20, max 100) - * name: Optional filter by name (partial, case-insensitive match) - * enabled: Optional filter by enabled status - * step_type: Optional filter by step type (built-ins: 'tool', 'llm') - * stage: Optional filter by stage ('pre' or 'post') - * execution: Optional filter by execution ('server' or 'sdk') - * tag: Optional filter by tag - * db: Database session (injected) - * - * Returns: - * ListControlsResponse with control summaries and pagination info - * - * Example: - * GET /controls?limit=10&enabled=true&step_type=tool - */ - get: operations["list_controls_api_v1_controls_get"]; - /** - * Create a new control - * @description Create a new control with a unique name and empty data. - * - * Controls define protection logic and can be added to policies. - * Use the PUT /{control_id}/data endpoint to set control configuration. - * - * Args: - * request: Control creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreateControlResponse with the new control's ID - * - * Raises: - * HTTPException 409: Control with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations["create_control_api_v1_controls_put"]; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control details - * @description Retrieve a control by ID including its name and configuration data. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlResponse with control id, name, and data - * - * Raises: - * HTTPException 404: Control not found - */ - get: operations["get_control_api_v1_controls__control_id__get"]; - put?: never; - post?: never; - /** - * Delete a control - * @description Delete a control by ID. - * - * By default, deletion fails if the control is associated with any policy or agent. - * Use force=true to automatically dissociate and delete. - * - * Args: - * control_id: ID of the control to delete - * force: If true, remove associations before deleting - * db: Database session (injected) - * - * Returns: - * DeleteControlResponse with success flag and dissociation details - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: Control is in use (and force=false) - * HTTPException 500: Database error during deletion - */ - delete: operations["delete_control_api_v1_controls__control_id__delete"]; - options?: never; - head?: never; - /** - * Update control metadata - * @description Update control metadata (name and/or enabled status). - * - * This endpoint allows partial updates: - * - To rename: provide 'name' field - * - To enable/disable: provide 'enabled' field (updates the control's data) - * - * Args: - * control_id: ID of the control to update - * request: Fields to update (name, enabled) - * db: Database session (injected) - * - * Returns: - * PatchControlResponse with current control state - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: New name conflicts with existing control - * HTTPException 422: Cannot update enabled status (control has no data configured) - * HTTPException 500: Database error during update - */ - patch: operations["patch_control_api_v1_controls__control_id__patch"]; - trace?: never; - }; - "/api/v1/controls/{control_id}/data": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control configuration data - * @description Retrieve the configuration data for a control. - * - * Control data is a JSONB field that must follow the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlDataResponse with validated ControlDefinition - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 422: Control data is corrupted - */ - get: operations["get_control_data_api_v1_controls__control_id__data_get"]; - /** - * Update control configuration data - * @description Update the configuration data for a control. - * - * This replaces the entire data payload. The data is validated against - * the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * request: New control data (replaces existing) - * db: Database session (injected) - * - * Returns: - * SetControlDataResponse with success flag - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 500: Database error during update - */ - put: operations["set_control_data_api_v1_controls__control_id__data_put"]; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** List evaluator configs */ - get: operations["list_evaluator_configs_api_v1_evaluator_configs_get"]; - put?: never; - /** Create evaluator config */ - post: operations["create_evaluator_config_api_v1_evaluator_configs_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/evaluator-configs/{config_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get evaluator config */ - get: operations["get_evaluator_config_api_v1_evaluator_configs__config_id__get"]; - /** Update evaluator config */ - put: operations["update_evaluator_config_api_v1_evaluator_configs__config_id__put"]; - post?: never; - /** Delete evaluator config */ - delete: operations["delete_evaluator_config_api_v1_evaluator_configs__config_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/evaluators": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List available evaluators - * @description List all available evaluators. - * - * Returns metadata and JSON Schema for each built-in evaluator. - * - * Built-in evaluators: - * - **regex**: Regular expression pattern matching - * - **list**: List-based value matching with flexible logic - * - **json**: JSON validation with schema, types, constraints - * - **sql**: SQL query validation - * - * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. - */ - get: operations["get_evaluators_api_v1_evaluators_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ingest Events - * @description Ingest batched control execution events. - * - * Events are stored directly to the database with ~5-20ms latency. - * - * Args: - * request: Batch of events to ingest - * ingestor: Event ingestor (injected) - * - * Returns: - * BatchEventsResponse with counts of received/processed/dropped - */ - post: operations["ingest_events_api_v1_observability_events_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/events/query": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Query Events - * @description Query raw control execution events. - * - * Supports filtering by: - * - 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 - * - control_ids: Filter by controls - * - actions: Filter by actions (allow, deny, warn, log) - * - matched: Filter by matched status - * - check_stages: Filter by check stage (pre, post) - * - applies_to: Filter by call type (llm_call, tool_call) - * - start_time/end_time: Filter by time range - * - * Results are paginated with limit/offset. - * - * Args: - * request: Query parameters - * store: Event store (injected) - * - * Returns: - * EventQueryResponse with matching events and pagination info - */ - post: operations["query_events_api_v1_observability_events_query_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Stats - * @description Get agent-level aggregated statistics. - * - * Returns totals across all controls plus per-control breakdown. - * Use /stats/controls/{control_id} for single control stats. - * - * Args: - * agent_uuid: 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) - * - * Returns: - * StatsResponse with agent-level totals and per-control breakdown - */ - get: operations["get_stats_api_v1_observability_stats_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/stats/controls/{control_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Control Stats - * @description Get statistics for a single control. - * - * Returns stats for the specified control with optional time-series. - * - * Args: - * control_id: Control ID to get stats for - * agent_uuid: 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) - * - * Returns: - * ControlStatsResponse with control stats and optional timeseries - */ - get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Status - * @description Get observability system status. - * - * Returns basic health information. - */ - get: operations["get_status_api_v1_observability_status_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/policies": { - 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. - * - * 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; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + '/api/v1/agents': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** + * List all agents + * @description List all registered agents with cursor-based pagination. + * + * Returns a summary of each agent including ID, name, policy associations, + * and counts of registered steps and evaluators. + * + * Args: + * cursor: Optional cursor for pagination (UUID of last agent from previous page) + * limit: Pagination limit (default 20, max 100) + * name: Optional name filter (case-insensitive partial match) + * db: Database session (injected) + * + * Returns: + * ListAgentsResponse with agent summaries and pagination info + */ + get: operations['list_agents_api_v1_agents_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/initAgent': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Initialize or update an agent + * @description Register a new agent or update an existing agent's steps and metadata. + * + * 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 UUID exists with a different name, returns 409 Conflict (no renames) + * + * Step versioning: When step schemas change (input_schema or output_schema), + * a new version is created automatically. + * + * Args: + * request: Agent metadata and step schemas + * db: Database session (injected) + * + * Returns: + * InitAgentResponse with created flag and active controls + * + * 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; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent details + * @description 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 + * db: Database session (injected) + * + * Returns: + * GetAgentResponse with agent metadata and step list + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 422: Agent data is corrupted + */ + get: operations['get_agent_api_v1_agents__agent_id__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify agent (remove steps/evaluators) + * @description Remove steps and/or evaluators from an agent. + * + * This is the complement to initAgent which only adds items. + * Removals are idempotent - attempting to remove non-existent items is not an error. + * + * Args: + * agent_id: UUID of the agent + * request: Lists of step/evaluator identifiers to remove + * db: Database session (injected) + * + * Returns: + * PatchAgentResponse with lists of actually removed items + * + * Raises: + * 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}/controls': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's active controls + * @description List all protection controls active for an agent. + * + * Controls include the union of policy-derived and directly associated controls. + * + * Args: + * agent_id: UUID of the agent + * db: Database session (injected) + * + * Returns: + * AgentControlsResponse with list of active controls + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations['list_agent_controls_api_v1_agents__agent_id__controls_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}/controls/{control_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate control directly with agent + * @description Associate a control directly with an agent (idempotent). + */ + post: operations['add_agent_control_api_v1_agents__agent_id__controls__control_id__post']; + /** + * Remove direct control association from agent + * @description Remove a direct control association from an agent (idempotent). + */ + delete: operations['remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}/evaluators': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's registered evaluator schemas + * @description List all evaluator schemas registered with an agent. + * + * Evaluator schemas are registered via initAgent and used for: + * - Config validation when creating Controls + * - UI to display available config options + * + * Args: + * agent_id: UUID of the agent + * cursor: Optional cursor for pagination (name of last evaluator from previous page) + * limit: Pagination limit (default 20, max 100) + * db: Database session (injected) + * + * Returns: + * ListEvaluatorsResponse with evaluator schemas and pagination + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations['list_agent_evaluators_api_v1_agents__agent_id__evaluators_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}/evaluators/{evaluator_name}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get specific evaluator schema + * @description Get a specific evaluator schema registered with an agent. + * + * Args: + * agent_id: UUID of the agent + * evaluator_name: Name of the evaluator + * db: Database session (injected) + * + * Returns: + * EvaluatorSchemaItem with schema details + * + * Raises: + * HTTPException 404: Agent or evaluator not found + */ + get: operations['get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}/policies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policies associated with agent + * @description List policy IDs associated with an agent. + */ + get: operations['get_agent_policies_api_v1_agents__agent_id__policies_get']; + put?: never; + post?: never; + /** + * Remove all policy associations from agent + * @description Remove all policy associations from an agent. + */ + delete: operations['remove_all_agent_policies_api_v1_agents__agent_id__policies_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_id}/policies/{policy_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate policy with agent + * @description Associate a policy with an agent (idempotent). + */ + post: operations['add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post']; + /** + * Remove policy association from agent + * @description Remove a policy association from an agent (idempotent). + */ + delete: operations['remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/controls': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all controls + * @description List all controls with optional filtering and cursor-based pagination. + * + * Controls are returned ordered by ID descending (newest first). + * + * Args: + * cursor: ID of the last control from the previous page (for pagination) + * limit: Maximum number of controls to return (default 20, max 100) + * name: Optional filter by name (partial, case-insensitive match) + * enabled: Optional filter by enabled status + * step_type: Optional filter by step type (built-ins: 'tool', 'llm') + * stage: Optional filter by stage ('pre' or 'post') + * execution: Optional filter by execution ('server' or 'sdk') + * tag: Optional filter by tag + * db: Database session (injected) + * + * Returns: + * ListControlsResponse with control summaries and pagination info + * + * Example: + * GET /controls?limit=10&enabled=true&step_type=tool + */ + get: operations['list_controls_api_v1_controls_get']; + /** + * Create a new control + * @description Create a new control with a unique name and empty data. + * + * Controls define protection logic and can be added to policies. + * Use the PUT /{control_id}/data endpoint to set control configuration. + * + * Args: + * request: Control creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreateControlResponse with the new control's ID + * + * Raises: + * HTTPException 409: Control with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations['create_control_api_v1_controls_put']; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control details + * @description Retrieve a control by ID including its name and configuration data. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlResponse with control id, name, and data + * + * Raises: + * HTTPException 404: Control not found + */ + get: operations['get_control_api_v1_controls__control_id__get']; + put?: never; + post?: never; + /** + * Delete a control + * @description Delete a control by ID. + * + * By default, deletion fails if the control is associated with any policy or agent. + * Use force=true to automatically dissociate and delete. + * + * Args: + * control_id: ID of the control to delete + * force: If true, remove associations before deleting + * db: Database session (injected) + * + * Returns: + * DeleteControlResponse with success flag and dissociation details + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: Control is in use (and force=false) + * HTTPException 500: Database error during deletion + */ + delete: operations['delete_control_api_v1_controls__control_id__delete']; + options?: never; + head?: never; + /** + * Update control metadata + * @description Update control metadata (name and/or enabled status). + * + * This endpoint allows partial updates: + * - To rename: provide 'name' field + * - To enable/disable: provide 'enabled' field (updates the control's data) + * + * Args: + * control_id: ID of the control to update + * request: Fields to update (name, enabled) + * db: Database session (injected) + * + * Returns: + * PatchControlResponse with current control state + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: New name conflicts with existing control + * HTTPException 422: Cannot update enabled status (control has no data configured) + * HTTPException 500: Database error during update + */ + patch: operations['patch_control_api_v1_controls__control_id__patch']; + trace?: never; + }; + '/api/v1/controls/{control_id}/data': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control configuration data + * @description Retrieve the configuration data for a control. + * + * Control data is a JSONB field that must follow the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlDataResponse with validated ControlDefinition + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 422: Control data is corrupted + */ + get: operations['get_control_data_api_v1_controls__control_id__data_get']; + /** + * Update control configuration data + * @description Update the configuration data for a control. + * + * This replaces the entire data payload. The data is validated against + * the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * request: New control data (replaces existing) + * db: Database session (injected) + * + * Returns: + * SetControlDataResponse with success flag + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 500: Database error during update + */ + put: operations['set_control_data_api_v1_controls__control_id__data_put']; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** List evaluator configs */ + get: operations['list_evaluator_configs_api_v1_evaluator_configs_get']; + put?: never; + /** Create evaluator config */ + post: operations['create_evaluator_config_api_v1_evaluator_configs_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/evaluator-configs/{config_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get evaluator config */ + get: operations['get_evaluator_config_api_v1_evaluator_configs__config_id__get']; + /** Update evaluator config */ + put: operations['update_evaluator_config_api_v1_evaluator_configs__config_id__put']; + post?: never; + /** Delete evaluator config */ + delete: operations['delete_evaluator_config_api_v1_evaluator_configs__config_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/evaluators': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available evaluators + * @description List all available evaluators. + * + * Returns metadata and JSON Schema for each built-in evaluator. + * + * Built-in evaluators: + * - **regex**: Regular expression pattern matching + * - **list**: List-based value matching with flexible logic + * - **json**: JSON validation with schema, types, constraints + * - **sql**: SQL query validation + * + * Custom evaluators are registered per-agent via initAgent. + * Use GET /agents/{agent_id}/evaluators to list agent-specific schemas. + */ + get: operations['get_evaluators_api_v1_evaluators_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/events': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ingest Events + * @description Ingest batched control execution events. + * + * Events are stored directly to the database with ~5-20ms latency. + * + * Args: + * request: Batch of events to ingest + * ingestor: Event ingestor (injected) + * + * Returns: + * BatchEventsResponse with counts of received/processed/dropped + */ + post: operations['ingest_events_api_v1_observability_events_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/events/query': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Query Events + * @description Query raw control execution events. + * + * Supports filtering by: + * - 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 + * - control_ids: Filter by controls + * - actions: Filter by actions (allow, deny, warn, log) + * - matched: Filter by matched status + * - check_stages: Filter by check stage (pre, post) + * - applies_to: Filter by call type (llm_call, tool_call) + * - start_time/end_time: Filter by time range + * + * Results are paginated with limit/offset. + * + * Args: + * request: Query parameters + * store: Event store (injected) + * + * Returns: + * EventQueryResponse with matching events and pagination info + */ + post: operations['query_events_api_v1_observability_events_query_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Stats + * @description Get agent-level aggregated statistics. + * + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. + * + * Args: + * agent_uuid: 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) + * + * Returns: + * StatsResponse with agent-level totals and per-control breakdown + */ + get: operations['get_stats_api_v1_observability_stats_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/stats/controls/{control_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID to get stats for + * agent_uuid: 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) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations['get_control_stats_api_v1_observability_stats_controls__control_id__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Status + * @description Get observability system status. + * + * Returns basic health information. + */ + get: operations['get_status_api_v1_observability_status_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/policies': { + 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. + * + * 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; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - /** - * Agent - * @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. - * @example { - * "agent_description": "Handles customer inquiries and support tickets", - * "agent_id": "550e8400-e29b-41d4-a716-446655440000", - * "agent_metadata": { - * "environment": "production", - * "team": "support" - * }, - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * } - */ - Agent: { - /** - * Agent Created At - * @description ISO 8601 timestamp when agent was created - */ - agent_created_at?: string | null; - /** - * Agent Description - * @description Optional description of the agent's purpose - */ - agent_description?: string | null; - /** - * Agent Id - * Format: uuid - * @description Unique identifier for the agent (UUID format) - */ - agent_id: string; - /** - * Agent Metadata - * @description Free-form metadata dictionary for custom properties - */ - agent_metadata?: { - [key: string]: unknown; - } | null; - /** - * Agent Name - * @description Human-readable name for the agent - */ - agent_name: string; - /** - * Agent Updated At - * @description ISO 8601 timestamp when agent was last updated - */ - agent_updated_at?: string | null; - /** - * Agent Version - * @description Semantic version string (e.g. '1.0.0') - */ - agent_version?: string | null; - }; - /** AgentControlsResponse */ - AgentControlsResponse: { - /** - * Controls - * @description List of active controls associated with the agent - */ - controls: components["schemas"]["Control"][]; - }; - /** - * AgentRef - * @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 - */ - agent_name: string; - }; - /** - * AgentSummary - * @description Summary of an agent for list responses. - */ - AgentSummary: { - /** - * Active Controls Count - * @description Number of active controls for this agent - * @default 0 - */ - active_controls_count: number; - /** - * Agent Id - * @description UUID of the agent - */ - agent_id: string; - /** - * Agent Name - * @description Human-readable name of the agent - */ - agent_name: string; - /** - * Created At - * @description ISO 8601 timestamp when agent was created - */ - created_at?: string | null; - /** - * Evaluator Count - * @description Number of evaluators registered with the agent - * @default 0 - */ - evaluator_count: number; - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - /** - * Step Count - * @description Number of steps registered with the agent - * @default 0 - */ - step_count: number; - }; - /** AssocResponse */ - AssocResponse: { - /** - * Success - * @description Whether the association change succeeded - */ - success: boolean; - }; - /** - * BatchEventsRequest - * @description Request model for batch event ingestion. - * - * SDKs batch events and send them to the server periodically. - * This reduces HTTP overhead significantly (100x reduction). - * - * Attributes: - * events: List of control execution events to ingest - * @example { - * "events": [ - * { - * "action": "deny", - * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_id": 123, - * "control_name": "sql-injection-check", - * "matched": true, - * "span_id": "00f067aa0ba902b7", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * ] - * } - */ - BatchEventsRequest: { - /** - * Events - * @description List of events to ingest - */ - events: components["schemas"]["ControlExecutionEvent"][]; - }; - /** - * BatchEventsResponse - * @description Response model for batch event ingestion. - * - * Attributes: - * received: Number of events received - * enqueued: Number of events successfully enqueued - * dropped: Number of events dropped (queue full) - * status: Overall status ('queued', 'partial', 'failed') - */ - BatchEventsResponse: { - /** - * Dropped - * @description Number of events dropped - */ - dropped: number; - /** - * Enqueued - * @description Number of events enqueued - */ - enqueued: number; - /** - * Received - * @description Number of events received - */ - received: number; - /** - * Status - * @description Overall ingestion status - * @enum {string} - */ - status: "queued" | "partial" | "failed"; - }; - /** - * Control - * @description A control with identity and configuration. - * - * Note: Only fully-configured controls (with valid ControlDefinition) - * are returned from API endpoints. Unconfigured controls are filtered out. - */ - Control: { - control: components["schemas"]["ControlDefinition"]; - /** Id */ - id: number; - /** Name */ - name: string; - }; - /** - * ControlAction - * @description What to do when control matches. - */ - ControlAction: { - /** - * Decision - * @description Action to take when control is triggered - * @enum {string} - */ - decision: "allow" | "deny" | "warn" | "log"; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - ControlDefinition: { - /** @description What action to take when control matches */ - action: components["schemas"]["ControlAction"]; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components["schemas"]["EvaluatorSpec"]; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: "server" | "sdk"; - /** @description Which steps and stages this control applies to */ - scope?: components["schemas"]["ControlScope"]; - /** @description What data to select from the payload */ - selector: components["schemas"]["ControlSelector"]; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlExecutionEvent - * @description Represents a single control execution event. - * - * This is the core observability data model, capturing: - * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) - * - Context: agent, control, check stage, applies to - * - Result: action taken, whether matched, confidence score - * - Timing: when it happened, how long it took - * - Optional details: evaluator name, selector path, errors, metadata - * - * Attributes: - * 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) - * 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) - * applies_to: "llm_call" or "tool_call" - * action: The action taken (allow, deny, warn, log) - * matched: Whether the control evaluator matched - * confidence: Confidence score from the evaluator (0.0-1.0) - * timestamp: When the control was executed (UTC) - * execution_duration_ms: How long the control evaluation took - * evaluator_name: Name of the evaluator used - * selector_path: The selector path used to extract data - * error_message: Error message if evaluation failed - * metadata: Additional metadata for extensibility - * @example { - * "action": "deny", - * "agent_name": "my-agent", - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", - * "control_id": 123, - * "control_name": "sql-injection-check", - * "evaluator_name": "regex", - * "execution_duration_ms": 15.3, - * "matched": true, - * "selector_path": "input", - * "span_id": "00f067aa0ba902b7", - * "timestamp": "2025-01-09T10:30:00Z", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - */ - ControlExecutionEvent: { - /** - * Action - * @description Action taken by the control - * @enum {string} - */ - action: "allow" | "deny" | "warn" | "log"; - /** - * Agent Name - * @description Name of the agent (denormalized) - */ - agent_name: string; - /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent - */ - agent_uuid: string; - /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} - */ - applies_to: "llm_call" | "tool_call"; - /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} - */ - check_stage: "pre" | "post"; - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Control Execution Id - * @description Unique ID for this control execution - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control (denormalized) - */ - control_name: string; - /** - * Error Message - * @description Error message if evaluation failed - */ - error_message?: string | null; - /** - * Evaluator Name - * @description Name of the evaluator used - */ - evaluator_name?: string | null; - /** - * Execution Duration Ms - * @description Execution duration in milliseconds - */ - execution_duration_ms?: number | null; - /** - * Matched - * @description Whether the evaluator matched (True) or not (False) - */ - matched: boolean; - /** - * Metadata - * @description Additional metadata - */ - metadata?: { - [key: string]: unknown; - }; - /** - * Selector Path - * @description Selector path used to extract data - */ - selector_path?: string | null; - /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) - */ - span_id: string; - /** - * Timestamp - * Format: date-time - * @description When the control was executed (UTC) - */ - 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) - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control - */ - control_name: string; - /** @description Evaluator result (confidence, message, metadata) */ - result: components["schemas"]["EvaluatorResult"]; - }; - /** - * ControlScope - * @description Defines when a control applies to a Step. - * @example { - * "stages": [ - * "pre" - * ], - * "step_types": [ - * "tool" - * ] - * } - * @example { - * "step_names": [ - * "search_db", - * "fetch_user" - * ] - * } - * @example { - * "step_name_regex": "^db_.*" - * } - * @example { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * } - */ - ControlScope: { - /** - * Stages - * @description Evaluation stages this control applies to - */ - stages?: ("pre" | "post")[] | null; - /** - * Step Name Regex - * @description RE2 pattern matched with search() against step name - */ - step_name_regex?: string | null; - /** - * Step Names - * @description Exact step names this control applies to - */ - 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 - * @description Selects data from a Step payload. - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - * @example { - * "path": "output" - * } - * @example { - * "path": "context.user_id" - * } - * @example { - * "path": "input" - * } - * @example { - * "path": "*" - * } - * @example { - * "path": "name" - * } - * @example { - * "path": "output" - * } - */ - ControlSelector: { - /** - * Path - * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' - * @default * - */ - path: string | null; - }; - /** - * ControlStats - * @description Aggregated statistics for a single control. - * - * Attributes: - * control_id: Database ID of the control - * control_name: Name of the control - * execution_count: Total number of executions - * match_count: Number of times the control matched - * non_match_count: Number of times the control did not match - * allow_count: Number of allow actions - * deny_count: Number of deny actions - * warn_count: Number of warn actions - * log_count: Number of log actions - * error_count: Number of errors during evaluation - * avg_confidence: Average confidence score - * 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 - */ - control_id: number; - /** - * Control Name - * @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 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - */ - non_match_count: number; - /** - * Warn Count - * @description Warn actions - */ - warn_count: number; - }; - /** - * ControlStatsResponse - * @description Response model for control-level statistics. - * - * Contains stats for a single control (with optional timeseries). - * - * Attributes: - * agent_uuid: Agent UUID - * time_range: Time range used - * control_id: Control ID - * control_name: Control name - * stats: Control statistics (includes timeseries when requested) - */ - ControlStatsResponse: { - /** - * Agent Uuid - * Format: uuid - * @description Agent UUID - */ - agent_uuid: string; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - 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: { - /** - * Description - * @description Control description - */ - description?: string | null; - /** - * Enabled - * @description Whether control is enabled - * @default true - */ - enabled: boolean; - /** - * Execution - * @description 'server' or 'sdk' - */ - execution?: string | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - /** - * Stages - * @description Evaluation stages in scope - */ - stages?: string[] | null; - /** - * Step Types - * @description Step types in scope - */ - step_types?: string[] | null; - /** - * Tags - * @description Control tags - */ - tags?: string[]; - /** @description Agent using this control */ - used_by_agent?: components["schemas"]["AgentRef"] | null; - /** - * Used By Agents Count - * @description Number of unique agents using this control - * @default 0 - */ - used_by_agents_count: number; - }; - /** CreateControlRequest */ - CreateControlRequest: { - /** - * Name - * @description Unique control name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreateControlResponse */ - CreateControlResponse: { - /** - * Control Id - * @description Identifier of the created control - */ - control_id: number; - }; - /** - * CreateEvaluatorConfigRequest - * @description Request to create an evaluator config template. - */ - CreateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyRequest */ - CreatePolicyRequest: { - /** - * Name - * @description Unique policy name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyResponse */ - CreatePolicyResponse: { - /** - * Policy Id - * @description Identifier of the created policy - */ - policy_id: number; - }; - /** - * DeleteControlResponse - * @description Response for deleting a control. - */ - DeleteControlResponse: { - /** - * Dissociated From Agents - * @description Agent IDs the control was removed from before deletion - */ - dissociated_from_agents?: string[]; - /** - * Dissociated From Policies - * @description Policy IDs the control was removed from before deletion - */ - dissociated_from_policies?: number[]; - /** - * Success - * @description Whether the control was deleted - */ - success: boolean; - }; - /** - * DeleteEvaluatorConfigResponse - * @description Response for deleting an evaluator config. - */ - DeleteEvaluatorConfigResponse: { - /** - * Success - * @description Whether the evaluator config was deleted - */ - success: boolean; - }; - /** - * EvaluationRequest - * @description Request model for evaluation analysis. - * - * Used to analyze agent interactions for safety violations, - * policy compliance, and control rules. - * - * Attributes: - * agent_uuid: UUID 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", - * "stage": "pre", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "type": "llm" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "post", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "output": "I cannot share sensitive payment information.", - * "type": "llm" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "pre", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "type": "tool" - * } - * } - * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", - * "stage": "post", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "output": { - * "results": [] - * }, - * "type": "tool" - * } - * } - */ - EvaluationRequest: { - /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent making the evaluation request - */ - agent_uuid: string; - /** - * Stage - * @description Evaluation stage: 'pre' or 'post' - * @enum {string} - */ - stage: "pre" | "post"; - /** @description Agent step payload to evaluate */ - step: components["schemas"]["Step"]; - }; - /** - * EvaluationResponse - * @description Response model from evaluation analysis (server-side). - * - * This is what the server returns. The SDK may transform this - * into an EvaluationResult for client convenience. - * - * Attributes: - * is_safe: Whether the content is considered safe - * confidence: Confidence score between 0.0 and 1.0 - * reason: Optional explanation for the decision - * matches: List of controls that matched/triggered (if any) - * errors: List of controls that failed during evaluation (if any) - * non_matches: List of controls that were evaluated but did not match (if any) - */ - EvaluationResponse: { - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Errors - * @description List of controls that failed during evaluation (if any) - */ - 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; - /** - * Non Matches - * @description List of controls that were evaluated but did not match (if any) - */ - non_matches?: components["schemas"]["ControlMatch"][] | null; - /** - * Reason - * @description Explanation for the decision - */ - reason?: string | null; - }; - /** - * EvaluatorConfigItem - * @description Evaluator config template stored in the server. - */ - EvaluatorConfigItem: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Created At - * @description ISO 8601 created timestamp - */ - created_at?: string | null; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Id - * @description Evaluator config ID - */ - id: number; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Updated At - * @description ISO 8601 updated timestamp - */ - updated_at?: string | null; - }; - /** - * EvaluatorInfo - * @description Information about a registered evaluator. - */ - EvaluatorInfo: { - /** - * Config Schema - * @description JSON Schema for config - */ - 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 - */ - requires_api_key: boolean; - /** - * Timeout Ms - * @description Default timeout in milliseconds - */ - timeout_ms: number; - /** - * Version - * @description Evaluator version - */ - version: string; - }; - /** - * EvaluatorResult - * @description Result from a control evaluator. - * - * The `error` field indicates evaluator failures, NOT validation failures: - * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors - * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures - * - * When `error` is set, `matched` must be False (fail-open on evaluator errors). - * When `error` is None, `matched` reflects the actual validation result. - * - * This distinction allows: - * - Clients to distinguish "data violated rules" from "evaluator is broken" - * - Observability systems to monitor evaluator health separately from validation outcomes - */ - EvaluatorResult: { - /** - * 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 - */ - message?: string | null; - /** - * Metadata - * @description Additional result metadata - */ - metadata?: { - [key: string]: unknown; - } | null; - }; - /** - * EvaluatorSchema - * @description Schema for a custom evaluator registered with an agent. - * - * Custom evaluators are Evaluator classes deployed with the engine. - * This schema is registered via initAgent for validation and UI purposes. - */ - EvaluatorSchema: { - /** - * Config Schema - * @description JSON Schema for evaluator config validation - */ - config_schema?: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Name - * @description Unique evaluator name - */ - name: string; - }; - /** - * EvaluatorSchemaItem - * @description Evaluator schema summary for list response. - */ - EvaluatorSchemaItem: { - /** 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 - * @description Request model for querying raw events. - * - * Supports filtering by various criteria and pagination. - * - * Attributes: - * 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 - * 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: { - /** - * Actions - * @description Filter by actions - */ - actions?: ("allow" | "deny" | "warn" | "log")[] | null; - /** - * Agent Uuid - * @description Filter by agent UUID - */ - agent_uuid?: 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; - /** - * Control Execution Id - * @description Filter by specific event ID - */ - control_execution_id?: string | null; - /** - * Control Ids - * @description Filter by control IDs - */ - control_ids?: number[] | null; - /** - * End Time - * @description Filter events before this time - */ - end_time?: string | null; - /** - * Limit - * @description Maximum events - * @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 - * @description Response model for event queries. - * - * Attributes: - * events: List of matching events - * total: Total number of matching events (for pagination) - * limit: Limit used in query - * offset: Offset used in query - */ - EventQueryResponse: { - /** - * Events - * @description Matching events - */ - events: components["schemas"]["ControlExecutionEvent"][]; - /** - * Limit - * @description Limit used in query - */ - limit: number; - /** - * Offset - * @description Offset used in query - */ - offset: number; - /** - * Total - * @description Total matching events - */ - total: number; - }; - /** GetAgentPoliciesResponse */ - GetAgentPoliciesResponse: { - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - }; - /** - * GetAgentResponse - * @description Response containing agent details and registered steps. - */ - GetAgentResponse: { - /** @description Agent metadata */ - agent: components["schemas"]["Agent"]; - /** - * 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: { - /** @description Control data payload */ - data: components["schemas"]["ControlDefinition"]; - }; - /** - * GetControlResponse - * @description Response containing control details. - */ - GetControlResponse: { - /** @description Control configuration data (None if not yet configured) */ - data?: components["schemas"]["ControlDefinition"] | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - }; - /** - * GetPolicyControlsResponse - * @description Response containing control IDs associated with a policy. - */ - GetPolicyControlsResponse: { - /** - * Control Ids - * @description List of control IDs associated with the policy - */ - control_ids: number[]; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components["schemas"]["ValidationError"][]; - }; - /** - * HealthResponse - * @description Health check response model. - * - * Attributes: - * status: Current health status (e.g., "healthy", "degraded", "unhealthy") - * version: Application version - */ - HealthResponse: { - /** Status */ - status: string; - /** Version */ - version: string; - }; - /** - * 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" - * }, - * "evaluators": [ - * { - * "config_schema": { - * "properties": { - * "sensitivity": { - * "type": "string" - * } - * }, - * "type": "object" - * }, - * "description": "Detects PII in text", - * "name": "pii-detector" - * } - * ], - * "steps": [ - * { - * "input_schema": { - * "query": { - * "type": "string" - * } - * }, - * "name": "search_kb", - * "output_schema": { - * "results": { - * "type": "array" - * } - * }, - * "type": "tool" - * } - * ] - * } - */ - InitAgentRequest: { - /** @description Agent metadata including ID, name, and version */ - agent: components["schemas"]["Agent"]; - /** - * Evaluators - * @description Custom evaluator schemas for config validation - */ - evaluators?: components["schemas"]["EvaluatorSchema"][]; - /** - * Force Replace - * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. - * @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 - */ - controls?: components["schemas"]["Control"][]; - /** - * Created - * @description True if agent was newly created, False if updated - */ - created: boolean; - }; - JSONObject: { - [key: string]: components["schemas"]["JSONValue"]; - }; - /** @description Any JSON value */ - JSONValue: unknown; - /** - * ListAgentsResponse - * @description Response for listing agents. - */ - ListAgentsResponse: { - /** - * Agents - * @description List of agent summaries - */ - agents: components["schemas"]["AgentSummary"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListControlsResponse - * @description Response for listing controls. - */ - ListControlsResponse: { - /** - * Controls - * @description List of control summaries - */ - controls: components["schemas"]["ControlSummary"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListEvaluatorConfigsResponse - * @description Response for listing evaluator configs. - */ - ListEvaluatorConfigsResponse: { - /** - * Evaluator Configs - * @description List of evaluator configs - */ - evaluator_configs: components["schemas"]["EvaluatorConfigItem"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListEvaluatorsResponse - * @description Response for listing agent's evaluator schemas. - */ - ListEvaluatorsResponse: { - /** Evaluators */ - evaluators: components["schemas"]["EvaluatorSchemaItem"][]; - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * PaginationInfo - * @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; - /** - * Next Cursor - * @description Cursor for fetching the next page (null if no more pages) - */ - next_cursor?: string | null; - /** - * Total - * @description Total number of items - */ - total: number; - }; - /** - * PatchAgentRequest - * @description Request to modify an agent (remove steps/evaluators). - */ - PatchAgentRequest: { - /** - * 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: { - /** - * 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: { - /** - * Enabled - * @description Enable or disable the control - */ - enabled?: boolean | null; - /** - * Name - * @description New name for the control - */ - name?: string | null; - }; - /** - * PatchControlResponse - * @description Response from control metadata update. - */ - PatchControlResponse: { - /** - * Enabled - * @description Current enabled status (if control has data configured) - */ - enabled?: boolean | null; - /** - * Name - * @description Current control name (may have changed) - */ - name: string; - /** - * Success - * @description Whether the update succeeded - */ - success: boolean; - }; - /** - * RemoveAgentControlResponse - * @description Response for removing a direct agent-control association. - */ - RemoveAgentControlResponse: { - /** - * Control Still Active - * @description True if the control remains active via policy association(s) - */ - control_still_active: boolean; - /** - * Removed Direct Association - * @description True if a direct agent-control link was removed - */ - removed_direct_association: boolean; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * SetControlDataRequest - * @description Request to update control configuration data. - */ - SetControlDataRequest: { - /** @description Control configuration data (replaces existing) */ - data: components["schemas"]["ControlDefinition"]; - }; - /** SetControlDataResponse */ - SetControlDataResponse: { - /** - * Success - * @description Whether the control data was updated - */ - success: boolean; - }; - /** - * StatsResponse - * @description Response model for agent-level aggregated statistics. - * - * Contains agent-level totals (with optional timeseries) and per-control breakdown. - * - * Attributes: - * agent_uuid: Agent UUID - * 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_uuid: string; - /** - * Controls - * @description Per-control breakdown - */ - controls: components["schemas"]["ControlStats"][]; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** @description Agent-level aggregate statistics */ - totals: components["schemas"]["StatsTotals"]; - }; - /** - * StatsTotals - * @description Agent-level aggregate statistics. - * - * Invariant: execution_count = match_count + non_match_count + error_count - * - * Matches have actions (allow, deny, warn, log) tracked in action_counts. - * sum(action_counts.values()) == match_count - * - * Attributes: - * execution_count: Total executions across all controls - * match_count: Total matches across all controls (evaluator matched) - * non_match_count: Total non-matches across all controls (evaluator didn't match) - * error_count: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions - * 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 - */ - execution_count: number; - /** - * Match Count - * @description Total matches - * @default 0 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - * @default 0 - */ - non_match_count: number; - /** - * Timeseries - * @description Time-series data points (only when include_timeseries=true) - */ - timeseries?: components["schemas"]["TimeseriesBucket"][] | null; - }; - /** - * Step - * @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 Output content for this step (None for pre-checks) */ - output?: components["schemas"]["JSONValue"] | null; - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * StepKey - * @description Identifies a registered step schema by type and name. - */ - StepKey: { - /** - * Name - * @description Registered step name - */ - name: string; - /** - * Type - * @description Step type - */ - type: string; - }; - /** - * StepSchema - * @description Schema for a registered agent step. - * @example { - * "description": "Search the internal knowledge base", - * "input_schema": { - * "query": { - * "description": "Search query", - * "type": "string" - * } - * }, - * "name": "search_knowledge_base", - * "output_schema": { - * "results": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "type": "tool" - * } - * @example { - * "description": "Customer support response generation", - * "input_schema": { - * "messages": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "name": "support-answer", - * "output_schema": { - * "text": { - * "type": "string" - * } - * }, - * "type": "llm" - * } - */ - StepSchema: { - /** - * Description - * @description Optional description of the step - */ - description?: string | null; - /** - * Input Schema - * @description JSON schema describing step input - */ - 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 - */ - output_schema?: { - [key: string]: unknown; - } | null; - /** - * Type - * @description Step type for this schema (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * TimeseriesBucket - * @description Single data point in a time-series. - * - * Represents aggregated metrics for a single time bucket. - * - * Attributes: - * timestamp: Start time of the bucket (UTC, always timezone-aware) - * execution_count: Total executions in this bucket - * match_count: Number of matches in this bucket - * non_match_count: Number of non-matches in this bucket - * error_count: Number of errors in this bucket - * action_counts: Breakdown of actions for matched executions - * avg_confidence: Average confidence score (None if no executions) - * avg_duration_ms: Average execution duration in milliseconds (None if no data) - */ - TimeseriesBucket: { - /** - * 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) - */ - avg_duration_ms?: number | null; - /** - * Error Count - * @description Errors in bucket - */ - error_count: number; - /** - * Execution Count - * @description Total executions in bucket - */ - execution_count: number; - /** - * Match Count - * @description Matches in bucket - */ - match_count: number; - /** - * Non Match Count - * @description Non-matches in bucket - */ - non_match_count: number; - /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) - */ - timestamp: string; - }; - /** - * UpdateEvaluatorConfigRequest - * @description Request to replace an evaluator config template. - */ - UpdateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - 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 */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * Agent + * @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. + * @example { + * "agent_description": "Handles customer inquiries and support tickets", + * "agent_id": "550e8400-e29b-41d4-a716-446655440000", + * "agent_metadata": { + * "environment": "production", + * "team": "support" + * }, + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * } + */ + Agent: { + /** + * Agent Created At + * @description ISO 8601 timestamp when agent was created + */ + agent_created_at?: string | null; + /** + * Agent Description + * @description Optional description of the agent's purpose + */ + agent_description?: string | null; + /** + * Agent Id + * Format: uuid + * @description Unique identifier for the agent (UUID format) + */ + agent_id: string; + /** + * Agent Metadata + * @description Free-form metadata dictionary for custom properties + */ + agent_metadata?: { + [key: string]: unknown; + } | null; + /** + * Agent Name + * @description Human-readable name for the agent + */ + agent_name: string; + /** + * Agent Updated At + * @description ISO 8601 timestamp when agent was last updated + */ + agent_updated_at?: string | null; + /** + * Agent Version + * @description Semantic version string (e.g. '1.0.0') + */ + agent_version?: string | null; + }; + /** AgentControlsResponse */ + AgentControlsResponse: { + /** + * Controls + * @description List of active controls associated with the agent + */ + controls: components['schemas']['Control'][]; + }; + /** + * AgentRef + * @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 + */ + agent_name: string; + }; + /** + * AgentSummary + * @description Summary of an agent for list responses. + */ + AgentSummary: { + /** + * Active Controls Count + * @description Number of active controls for this agent + * @default 0 + */ + active_controls_count: number; + /** + * Agent Id + * @description UUID of the agent + */ + agent_id: string; + /** + * Agent Name + * @description Human-readable name of the agent + */ + agent_name: string; + /** + * Created At + * @description ISO 8601 timestamp when agent was created + */ + created_at?: string | null; + /** + * Evaluator Count + * @description Number of evaluators registered with the agent + * @default 0 + */ + evaluator_count: number; + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + /** + * Step Count + * @description Number of steps registered with the agent + * @default 0 + */ + step_count: number; + }; + /** AssocResponse */ + AssocResponse: { + /** + * Success + * @description Whether the association change succeeded + */ + success: boolean; + }; + /** + * BatchEventsRequest + * @description Request model for batch event ingestion. + * + * SDKs batch events and send them to the server periodically. + * This reduces HTTP overhead significantly (100x reduction). + * + * Attributes: + * events: List of control execution events to ingest + * @example { + * "events": [ + * { + * "action": "deny", + * "agent_name": "my-agent", + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_id": 123, + * "control_name": "sql-injection-check", + * "matched": true, + * "span_id": "00f067aa0ba902b7", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * ] + * } + */ + BatchEventsRequest: { + /** + * Events + * @description List of events to ingest + */ + events: components['schemas']['ControlExecutionEvent'][]; + }; + /** + * BatchEventsResponse + * @description Response model for batch event ingestion. + * + * Attributes: + * received: Number of events received + * enqueued: Number of events successfully enqueued + * dropped: Number of events dropped (queue full) + * status: Overall status ('queued', 'partial', 'failed') + */ + BatchEventsResponse: { + /** + * Dropped + * @description Number of events dropped + */ + dropped: number; + /** + * Enqueued + * @description Number of events enqueued + */ + enqueued: number; + /** + * Received + * @description Number of events received + */ + received: number; + /** + * Status + * @description Overall ingestion status + * @enum {string} + */ + status: 'queued' | 'partial' | 'failed'; + }; + /** + * Control + * @description A control with identity and configuration. + * + * Note: Only fully-configured controls (with valid ControlDefinition) + * are returned from API endpoints. Unconfigured controls are filtered out. + */ + Control: { + control: components['schemas']['ControlDefinition']; + /** Id */ + id: number; + /** Name */ + name: string; + }; + /** + * ControlAction + * @description What to do when control matches. + */ + ControlAction: { + /** + * Decision + * @description Action to take when control is triggered + * @enum {string} + */ + decision: 'allow' | 'deny' | 'warn' | 'log'; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + ControlDefinition: { + /** @description What action to take when control matches */ + action: components['schemas']['ControlAction']; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components['schemas']['EvaluatorSpec']; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: 'server' | 'sdk'; + /** @description Which steps and stages this control applies to */ + scope?: components['schemas']['ControlScope']; + /** @description What data to select from the payload */ + selector: components['schemas']['ControlSelector']; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlExecutionEvent + * @description Represents a single control execution event. + * + * This is the core observability data model, capturing: + * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) + * - Context: agent, control, check stage, applies to + * - Result: action taken, whether matched, confidence score + * - Timing: when it happened, how long it took + * - Optional details: evaluator name, selector path, errors, metadata + * + * Attributes: + * 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) + * 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) + * applies_to: "llm_call" or "tool_call" + * action: The action taken (allow, deny, warn, log) + * matched: Whether the control evaluator matched + * confidence: Confidence score from the evaluator (0.0-1.0) + * timestamp: When the control was executed (UTC) + * execution_duration_ms: How long the control evaluation took + * evaluator_name: Name of the evaluator used + * selector_path: The selector path used to extract data + * error_message: Error message if evaluation failed + * metadata: Additional metadata for extensibility + * @example { + * "action": "deny", + * "agent_name": "my-agent", + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", + * "control_id": 123, + * "control_name": "sql-injection-check", + * "evaluator_name": "regex", + * "execution_duration_ms": 15.3, + * "matched": true, + * "selector_path": "input", + * "span_id": "00f067aa0ba902b7", + * "timestamp": "2025-01-09T10:30:00Z", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + */ + ControlExecutionEvent: { + /** + * Action + * @description Action taken by the control + * @enum {string} + */ + action: 'allow' | 'deny' | 'warn' | 'log'; + /** + * Agent Name + * @description Name of the agent (denormalized) + */ + agent_name: string; + /** + * Agent Uuid + * Format: uuid + * @description UUID of the agent + */ + agent_uuid: string; + /** + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} + */ + applies_to: 'llm_call' | 'tool_call'; + /** + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} + */ + check_stage: 'pre' | 'post'; + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control (denormalized) + */ + control_name: string; + /** + * Error Message + * @description Error message if evaluation failed + */ + error_message?: string | null; + /** + * Evaluator Name + * @description Name of the evaluator used + */ + evaluator_name?: string | null; + /** + * Execution Duration Ms + * @description Execution duration in milliseconds + */ + execution_duration_ms?: number | null; + /** + * Matched + * @description Whether the evaluator matched (True) or not (False) + */ + matched: boolean; + /** + * Metadata + * @description Additional metadata + */ + metadata?: { + [key: string]: unknown; + }; + /** + * Selector Path + * @description Selector path used to extract data + */ + selector_path?: string | null; + /** + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + */ + span_id: string; + /** + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) + */ + 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) + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control + */ + control_name: string; + /** @description Evaluator result (confidence, message, metadata) */ + result: components['schemas']['EvaluatorResult']; + }; + /** + * ControlScope + * @description Defines when a control applies to a Step. + * @example { + * "stages": [ + * "pre" + * ], + * "step_types": [ + * "tool" + * ] + * } + * @example { + * "step_names": [ + * "search_db", + * "fetch_user" + * ] + * } + * @example { + * "step_name_regex": "^db_.*" + * } + * @example { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * } + */ + ControlScope: { + /** + * Stages + * @description Evaluation stages this control applies to + */ + stages?: ('pre' | 'post')[] | null; + /** + * Step Name Regex + * @description RE2 pattern matched with search() against step name + */ + step_name_regex?: string | null; + /** + * Step Names + * @description Exact step names this control applies to + */ + 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 + * @description Selects data from a Step payload. + * + * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" + * meaning the entire Step object. + * @example { + * "path": "output" + * } + * @example { + * "path": "context.user_id" + * } + * @example { + * "path": "input" + * } + * @example { + * "path": "*" + * } + * @example { + * "path": "name" + * } + * @example { + * "path": "output" + * } + */ + ControlSelector: { + /** + * Path + * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' + * @default * + */ + path: string | null; + }; + /** + * ControlStats + * @description Aggregated statistics for a single control. + * + * Attributes: + * control_id: Database ID of the control + * control_name: Name of the control + * execution_count: Total number of executions + * match_count: Number of times the control matched + * non_match_count: Number of times the control did not match + * allow_count: Number of allow actions + * deny_count: Number of deny actions + * warn_count: Number of warn actions + * log_count: Number of log actions + * error_count: Number of errors during evaluation + * avg_confidence: Average confidence score + * 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 + */ + control_id: number; + /** + * Control Name + * @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 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + */ + non_match_count: number; + /** + * Warn Count + * @description Warn actions + */ + warn_count: number; + }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_uuid: Agent UUID + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Uuid + * Format: uuid + * @description Agent UUID + */ + agent_uuid: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + 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: { + /** + * Description + * @description Control description + */ + description?: string | null; + /** + * Enabled + * @description Whether control is enabled + * @default true + */ + enabled: boolean; + /** + * Execution + * @description 'server' or 'sdk' + */ + execution?: string | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + /** + * Stages + * @description Evaluation stages in scope + */ + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; + /** + * Tags + * @description Control tags + */ + tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components['schemas']['AgentRef'] | null; + /** + * Used By Agents Count + * @description Number of unique agents using this control + * @default 0 + */ + used_by_agents_count: number; + }; + /** CreateControlRequest */ + CreateControlRequest: { + /** + * Name + * @description Unique control name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreateControlResponse */ + CreateControlResponse: { + /** + * Control Id + * @description Identifier of the created control + */ + control_id: number; + }; + /** + * CreateEvaluatorConfigRequest + * @description Request to create an evaluator config template. + */ + CreateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyRequest */ + CreatePolicyRequest: { + /** + * Name + * @description Unique policy name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyResponse */ + CreatePolicyResponse: { + /** + * Policy Id + * @description Identifier of the created policy + */ + policy_id: number; + }; + /** + * DeleteControlResponse + * @description Response for deleting a control. + */ + DeleteControlResponse: { + /** + * Dissociated From Agents + * @description Agent IDs the control was removed from before deletion + */ + dissociated_from_agents?: string[]; + /** + * Dissociated From Policies + * @description Policy IDs the control was removed from before deletion + */ + dissociated_from_policies?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; + }; + /** + * DeleteEvaluatorConfigResponse + * @description Response for deleting an evaluator config. + */ + DeleteEvaluatorConfigResponse: { + /** + * Success + * @description Whether the evaluator config was deleted + */ + success: boolean; + }; + /** + * EvaluationRequest + * @description Request model for evaluation analysis. + * + * Used to analyze agent interactions for safety violations, + * policy compliance, and control rules. + * + * Attributes: + * agent_uuid: UUID 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", + * "stage": "pre", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "type": "llm" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "post", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "output": "I cannot share sensitive payment information.", + * "type": "llm" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "pre", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "type": "tool" + * } + * } + * @example { + * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "stage": "post", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "output": { + * "results": [] + * }, + * "type": "tool" + * } + * } + */ + EvaluationRequest: { + /** + * Agent Uuid + * Format: uuid + * @description UUID of the agent making the evaluation request + */ + agent_uuid: string; + /** + * Stage + * @description Evaluation stage: 'pre' or 'post' + * @enum {string} + */ + stage: 'pre' | 'post'; + /** @description Agent step payload to evaluate */ + step: components['schemas']['Step']; + }; + /** + * EvaluationResponse + * @description Response model from evaluation analysis (server-side). + * + * This is what the server returns. The SDK may transform this + * into an EvaluationResult for client convenience. + * + * Attributes: + * is_safe: Whether the content is considered safe + * confidence: Confidence score between 0.0 and 1.0 + * reason: Optional explanation for the decision + * matches: List of controls that matched/triggered (if any) + * errors: List of controls that failed during evaluation (if any) + * non_matches: List of controls that were evaluated but did not match (if any) + */ + EvaluationResponse: { + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Errors + * @description List of controls that failed during evaluation (if any) + */ + 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; + /** + * Non Matches + * @description List of controls that were evaluated but did not match (if any) + */ + non_matches?: components['schemas']['ControlMatch'][] | null; + /** + * Reason + * @description Explanation for the decision + */ + reason?: string | null; + }; + /** + * EvaluatorConfigItem + * @description Evaluator config template stored in the server. + */ + EvaluatorConfigItem: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Created At + * @description ISO 8601 created timestamp + */ + created_at?: string | null; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Id + * @description Evaluator config ID + */ + id: number; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + /** + * Updated At + * @description ISO 8601 updated timestamp + */ + updated_at?: string | null; + }; + /** + * EvaluatorInfo + * @description Information about a registered evaluator. + */ + EvaluatorInfo: { + /** + * Config Schema + * @description JSON Schema for config + */ + 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 + */ + requires_api_key: boolean; + /** + * Timeout Ms + * @description Default timeout in milliseconds + */ + timeout_ms: number; + /** + * Version + * @description Evaluator version + */ + version: string; + }; + /** + * EvaluatorResult + * @description Result from a control evaluator. + * + * The `error` field indicates evaluator failures, NOT validation failures: + * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors + * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures + * + * When `error` is set, `matched` must be False (fail-open on evaluator errors). + * When `error` is None, `matched` reflects the actual validation result. + * + * This distinction allows: + * - Clients to distinguish "data violated rules" from "evaluator is broken" + * - Observability systems to monitor evaluator health separately from validation outcomes + */ + EvaluatorResult: { + /** + * 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 + */ + message?: string | null; + /** + * Metadata + * @description Additional result metadata + */ + metadata?: { + [key: string]: unknown; + } | null; + }; + /** + * EvaluatorSchema + * @description Schema for a custom evaluator registered with an agent. + * + * Custom evaluators are Evaluator classes deployed with the engine. + * This schema is registered via initAgent for validation and UI purposes. + */ + EvaluatorSchema: { + /** + * Config Schema + * @description JSON Schema for evaluator config validation + */ + config_schema?: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; + }; + /** + * EvaluatorSchemaItem + * @description Evaluator schema summary for list response. + */ + EvaluatorSchemaItem: { + /** 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 + * @description Request model for querying raw events. + * + * Supports filtering by various criteria and pagination. + * + * Attributes: + * 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 + * 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: { + /** + * Actions + * @description Filter by actions + */ + actions?: ('allow' | 'deny' | 'warn' | 'log')[] | null; + /** + * Agent Uuid + * @description Filter by agent UUID + */ + agent_uuid?: 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; + /** + * Control Execution Id + * @description Filter by specific event ID + */ + control_execution_id?: string | null; + /** + * Control Ids + * @description Filter by control IDs + */ + control_ids?: number[] | null; + /** + * End Time + * @description Filter events before this time + */ + end_time?: string | null; + /** + * Limit + * @description Maximum events + * @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 + * @description Response model for event queries. + * + * Attributes: + * events: List of matching events + * total: Total number of matching events (for pagination) + * limit: Limit used in query + * offset: Offset used in query + */ + EventQueryResponse: { + /** + * Events + * @description Matching events + */ + events: components['schemas']['ControlExecutionEvent'][]; + /** + * Limit + * @description Limit used in query + */ + limit: number; + /** + * Offset + * @description Offset used in query + */ + offset: number; + /** + * Total + * @description Total matching events + */ + total: number; + }; + /** GetAgentPoliciesResponse */ + GetAgentPoliciesResponse: { + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + }; + /** + * GetAgentResponse + * @description Response containing agent details and registered steps. + */ + GetAgentResponse: { + /** @description Agent metadata */ + agent: components['schemas']['Agent']; + /** + * 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: { + /** @description Control data payload */ + data: components['schemas']['ControlDefinition']; + }; + /** + * GetControlResponse + * @description Response containing control details. + */ + GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components['schemas']['ControlDefinition'] | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + }; + /** + * GetPolicyControlsResponse + * @description Response containing control IDs associated with a policy. + */ + GetPolicyControlsResponse: { + /** + * Control Ids + * @description List of control IDs associated with the policy + */ + control_ids: number[]; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components['schemas']['ValidationError'][]; + }; + /** + * HealthResponse + * @description Health check response model. + * + * Attributes: + * status: Current health status (e.g., "healthy", "degraded", "unhealthy") + * version: Application version + */ + HealthResponse: { + /** Status */ + status: string; + /** Version */ + version: string; + }; + /** + * 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" + * }, + * "evaluators": [ + * { + * "config_schema": { + * "properties": { + * "sensitivity": { + * "type": "string" + * } + * }, + * "type": "object" + * }, + * "description": "Detects PII in text", + * "name": "pii-detector" + * } + * ], + * "steps": [ + * { + * "input_schema": { + * "query": { + * "type": "string" + * } + * }, + * "name": "search_kb", + * "output_schema": { + * "results": { + * "type": "array" + * } + * }, + * "type": "tool" + * } + * ] + * } + */ + InitAgentRequest: { + /** @description Agent metadata including ID, name, and version */ + agent: components['schemas']['Agent']; + /** + * Evaluators + * @description Custom evaluator schemas for config validation + */ + evaluators?: components['schemas']['EvaluatorSchema'][]; + /** + * Force Replace + * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. + * @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 + */ + controls?: components['schemas']['Control'][]; + /** + * Created + * @description True if agent was newly created, False if updated + */ + created: boolean; + }; + JSONObject: { + [key: string]: components['schemas']['JSONValue']; + }; + /** @description Any JSON value */ + JSONValue: unknown; + /** + * ListAgentsResponse + * @description Response for listing agents. + */ + ListAgentsResponse: { + /** + * Agents + * @description List of agent summaries + */ + agents: components['schemas']['AgentSummary'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListControlsResponse + * @description Response for listing controls. + */ + ListControlsResponse: { + /** + * Controls + * @description List of control summaries + */ + controls: components['schemas']['ControlSummary'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListEvaluatorConfigsResponse + * @description Response for listing evaluator configs. + */ + ListEvaluatorConfigsResponse: { + /** + * Evaluator Configs + * @description List of evaluator configs + */ + evaluator_configs: components['schemas']['EvaluatorConfigItem'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListEvaluatorsResponse + * @description Response for listing agent's evaluator schemas. + */ + ListEvaluatorsResponse: { + /** Evaluators */ + evaluators: components['schemas']['EvaluatorSchemaItem'][]; + pagination: components['schemas']['PaginationInfo']; + }; + /** + * PaginationInfo + * @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; + /** + * Next Cursor + * @description Cursor for fetching the next page (null if no more pages) + */ + next_cursor?: string | null; + /** + * Total + * @description Total number of items + */ + total: number; + }; + /** + * PatchAgentRequest + * @description Request to modify an agent (remove steps/evaluators). + */ + PatchAgentRequest: { + /** + * 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: { + /** + * 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: { + /** + * Enabled + * @description Enable or disable the control + */ + enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; + }; + /** + * PatchControlResponse + * @description Response from control metadata update. + */ + PatchControlResponse: { + /** + * Enabled + * @description Current enabled status (if control has data configured) + */ + enabled?: boolean | null; + /** + * Name + * @description Current control name (may have changed) + */ + name: string; + /** + * Success + * @description Whether the update succeeded + */ + success: boolean; + }; + /** + * RemoveAgentControlResponse + * @description Response for removing a direct agent-control association. + */ + RemoveAgentControlResponse: { + /** + * Control Still Active + * @description True if the control remains active via policy association(s) + */ + control_still_active: boolean; + /** + * Removed Direct Association + * @description True if a direct agent-control link was removed + */ + removed_direct_association: boolean; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * SetControlDataRequest + * @description Request to update control configuration data. + */ + SetControlDataRequest: { + /** @description Control configuration data (replaces existing) */ + data: components['schemas']['ControlDefinition']; + }; + /** SetControlDataResponse */ + SetControlDataResponse: { + /** + * Success + * @description Whether the control data was updated + */ + success: boolean; + }; + /** + * StatsResponse + * @description Response model for agent-level aggregated statistics. + * + * Contains agent-level totals (with optional timeseries) and per-control breakdown. + * + * Attributes: + * agent_uuid: Agent UUID + * 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_uuid: string; + /** + * Controls + * @description Per-control breakdown + */ + controls: components['schemas']['ControlStats'][]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components['schemas']['StatsTotals']; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * 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 + */ + execution_count: number; + /** + * Match Count + * @description Total matches + * @default 0 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + * @default 0 + */ + non_match_count: number; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components['schemas']['TimeseriesBucket'][] | null; + }; + /** + * Step + * @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 Output content for this step (None for pre-checks) */ + output?: components['schemas']['JSONValue'] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * StepKey + * @description Identifies a registered step schema by type and name. + */ + StepKey: { + /** + * Name + * @description Registered step name + */ + name: string; + /** + * Type + * @description Step type + */ + type: string; + }; + /** + * StepSchema + * @description Schema for a registered agent step. + * @example { + * "description": "Search the internal knowledge base", + * "input_schema": { + * "query": { + * "description": "Search query", + * "type": "string" + * } + * }, + * "name": "search_knowledge_base", + * "output_schema": { + * "results": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "type": "tool" + * } + * @example { + * "description": "Customer support response generation", + * "input_schema": { + * "messages": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "name": "support-answer", + * "output_schema": { + * "text": { + * "type": "string" + * } + * }, + * "type": "llm" + * } + */ + StepSchema: { + /** + * Description + * @description Optional description of the step + */ + description?: string | null; + /** + * Input Schema + * @description JSON schema describing step input + */ + 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 + */ + output_schema?: { + [key: string]: unknown; + } | null; + /** + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * 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) + */ + avg_duration_ms?: number | null; + /** + * Error Count + * @description Errors in bucket + */ + error_count: number; + /** + * Execution Count + * @description Total executions in bucket + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + }; + /** + * UpdateEvaluatorConfigRequest + * @description Request to replace an evaluator config template. + */ + UpdateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + 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 */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - list_agents_api_v1_agents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - name?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of agent summaries */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListAgentsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - init_agent_api_v1_agents_initAgent_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["InitAgentRequest"]; - }; - }; - responses: { - /** @description Agent registration status with active controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["InitAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_agent_api_v1_agents__agent_id__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Agent metadata and registered steps */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - patch_agent_api_v1_agents__agent_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["PatchAgentRequest"]; - }; - }; - responses: { - /** @description Lists of removed items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PatchAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - list_agent_controls_api_v1_agents__agent_id__controls_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of controls from agent policy and direct associations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AgentControlsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - add_agent_control_api_v1_agents__agent_id__controls__control_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - 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_agent_control_api_v1_agents__agent_id__controls__control_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["RemoveAgentControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - list_agent_evaluators_api_v1_agents__agent_id__evaluators_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schemas registered with this agent */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListEvaluatorsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - 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"]; - }; - }; - }; - }; - get_agent_policies_api_v1_agents__agent_id__policies_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of policy IDs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetAgentPoliciesResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - remove_all_agent_policies_api_v1_agents__agent_id__policies_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"]["AssocResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - policy_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_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - policy_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"]; - }; - }; - }; - }; - list_controls_api_v1_controls_get: { - parameters: { - query?: { - /** @description Control ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by enabled status */ - enabled?: boolean | null; - /** @description Filter by step type (built-ins: 'tool', 'llm') */ - step_type?: string | null; - /** @description Filter by stage ('pre' or 'post') */ - stage?: string | null; - /** @description Filter by execution ('server' or 'sdk') */ - execution?: string | null; - /** @description Filter by tag */ - tag?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListControlsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - create_control_api_v1_controls_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateControlRequest"]; - }; - }; - responses: { - /** @description Created control ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreateControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - 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; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control metadata and configuration */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - delete_control_api_v1_controls__control_id__delete: { - parameters: { - query?: { - /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ - force?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation with dissociation info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - patch_control_api_v1_controls__control_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["PatchControlRequest"]; - }; - }; - responses: { - /** @description Updated control information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PatchControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_control_data_api_v1_controls__control_id__data_get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control data payload */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetControlDataResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - set_control_data_api_v1_controls__control_id__data_put: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetControlDataRequest"]; - }; - }; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SetControlDataResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - 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?: { - /** @description Evaluator config ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by evaluator name */ - evaluator?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of evaluator configs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListEvaluatorConfigsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - create_evaluator_config_api_v1_evaluator_configs_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateEvaluatorConfigRequest"]; - }; - }; - responses: { - /** @description Created evaluator config */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_evaluator_config_api_v1_evaluator_configs__config_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator config details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - update_evaluator_config_api_v1_evaluator_configs__config_id__put: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateEvaluatorConfigRequest"]; - }; - }; - responses: { - /** @description Updated evaluator config */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteEvaluatorConfigResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_evaluators_api_v1_evaluators_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Dictionary of evaluator name to evaluator info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: components["schemas"]["EvaluatorInfo"]; - }; - }; - }; - }; - }; - ingest_events_api_v1_observability_events_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["BatchEventsRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BatchEventsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - query_events_api_v1_observability_events_query_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["EventQueryRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EventQueryResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_stats_api_v1_observability_stats_get: { - parameters: { - query: { - agent_uuid: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; - include_timeseries?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["StatsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_control_stats_api_v1_observability_stats_controls__control_id__get: { - parameters: { - query: { - agent_uuid: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; - include_timeseries?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ControlStatsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_status_api_v1_observability_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: unknown; - }; - }; - }; - }; - }; - 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; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server health status */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; + list_agents_api_v1_agents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + name?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of agent summaries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListAgentsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + init_agent_api_v1_agents_initAgent_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['InitAgentRequest']; + }; + }; + responses: { + /** @description Agent registration status with active controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['InitAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_agent_api_v1_agents__agent_id__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Agent metadata and registered steps */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + patch_agent_api_v1_agents__agent_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['PatchAgentRequest']; + }; + }; + responses: { + /** @description Lists of removed items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PatchAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_agent_controls_api_v1_agents__agent_id__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of controls from agent policy and direct associations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AgentControlsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + add_agent_control_api_v1_agents__agent_id__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + 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_agent_control_api_v1_agents__agent_id__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['RemoveAgentControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_agent_evaluators_api_v1_agents__agent_id__evaluators_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schemas registered with this agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListEvaluatorsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + 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']; + }; + }; + }; + }; + get_agent_policies_api_v1_agents__agent_id__policies_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of policy IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetAgentPoliciesResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + remove_all_agent_policies_api_v1_agents__agent_id__policies_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']['AssocResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + policy_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_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_id: string; + policy_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']; + }; + }; + }; + }; + list_controls_api_v1_controls_get: { + parameters: { + query?: { + /** @description Control ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by enabled status */ + enabled?: boolean | null; + /** @description Filter by step type (built-ins: 'tool', 'llm') */ + step_type?: string | null; + /** @description Filter by stage ('pre' or 'post') */ + stage?: string | null; + /** @description Filter by execution ('server' or 'sdk') */ + execution?: string | null; + /** @description Filter by tag */ + tag?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListControlsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + create_control_api_v1_controls_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateControlRequest']; + }; + }; + responses: { + /** @description Created control ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['CreateControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + 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; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control metadata and configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + delete_control_api_v1_controls__control_id__delete: { + parameters: { + query?: { + /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + force?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation with dissociation info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeleteControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + patch_control_api_v1_controls__control_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['PatchControlRequest']; + }; + }; + responses: { + /** @description Updated control information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PatchControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_control_data_api_v1_controls__control_id__data_get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control data payload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetControlDataResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + set_control_data_api_v1_controls__control_id__data_put: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['SetControlDataRequest']; + }; + }; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SetControlDataResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + 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?: { + /** @description Evaluator config ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by evaluator name */ + evaluator?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of evaluator configs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListEvaluatorConfigsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + create_evaluator_config_api_v1_evaluator_configs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateEvaluatorConfigRequest']; + }; + }; + responses: { + /** @description Created evaluator config */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_evaluator_config_api_v1_evaluator_configs__config_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator config details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + update_evaluator_config_api_v1_evaluator_configs__config_id__put: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdateEvaluatorConfigRequest']; + }; + }; + responses: { + /** @description Updated evaluator config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeleteEvaluatorConfigResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_evaluators_api_v1_evaluators_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dictionary of evaluator name to evaluator info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: components['schemas']['EvaluatorInfo']; + }; + }; + }; + }; + }; + ingest_events_api_v1_observability_events_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['BatchEventsRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BatchEventsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + query_events_api_v1_observability_events_query_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EventQueryRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EventQueryResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_stats_api_v1_observability_stats_get: { + parameters: { + query: { + agent_uuid: string; + time_range?: + | '1m' + | '5m' + | '15m' + | '1h' + | '24h' + | '7d' + | '30d' + | '180d' + | '365d'; + include_timeseries?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['StatsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_uuid: string; + time_range?: + | '1m' + | '5m' + | '15m' + | '1h' + | '24h' + | '7d' + | '30d' + | '180d' + | '365d'; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ControlStatsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_status_api_v1_observability_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: unknown; + }; + }; + }; + }; + }; + 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; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server health status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HealthResponse']; }; + }; }; + }; } diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 2a1e32a2..5b0c3efa 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -27,7 +27,8 @@ export function useDeleteControlFlow({ children: ( Remove "{control.name}" from this agent? This only removes - the association for this agent and does not delete the control globally. + the association for this agent and does not delete the control + globally. ), labels: { confirm: 'Remove', cancel: 'Cancel' }, @@ -45,8 +46,7 @@ export function useDeleteControlFlow({ }, { onSuccess: (result: RemoveControlFromAgentResult) => { - const removedDirect = - result.removed_direct_association ?? true; + const removedDirect = result.removed_direct_association ?? true; const stillActive = result.control_still_active ?? false; if (!removedDirect) { From 998f5964006092bb754ed6e76591d0a08210c0f4 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 16:02:09 -0500 Subject: [PATCH 07/32] test(ui): align control removal and usage table specs --- ui/tests/agent-detail.spec.ts | 46 ++++++++++++++++++++-------------- ui/tests/control-store.spec.ts | 22 ++++++++-------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/ui/tests/agent-detail.spec.ts b/ui/tests/agent-detail.spec.ts index ffdc39cd..d75fe57c 100644 --- a/ui/tests/agent-detail.spec.ts +++ b/ui/tests/agent-detail.spec.ts @@ -412,17 +412,24 @@ test.describe('Agent Detail Page', () => { } ); - await mockedPage.route('**/api/v1/controls/*', async (route, request) => { - if (request.method() === 'DELETE') { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ success: true }), - }); - } else { - await route.continue(); + await mockedPage.route( + '**/api/v1/agents/*/controls/*', + async (route, request) => { + if (request.method() === 'DELETE') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + removed_direct_association: true, + control_still_active: false, + }), + }); + } else { + await route.continue(); + } } - }); + ); await mockedPage.goto(agentUrl); @@ -439,32 +446,33 @@ test.describe('Agent Detail Page', () => { await targetRow.scrollIntoViewIfNeeded(); const deleteButton = targetRow.getByRole('button', { - name: 'Delete control', + name: 'Remove control from agent', }); await deleteButton.click(); const confirmModal = mockedPage.getByRole('dialog', { - name: /Delete control\?/i, + name: /Remove control from agent\?/i, }); await expect(confirmModal).toBeVisible(); await expect( - confirmModal.getByText(/This action cannot be undone/) + confirmModal.getByText(/does not delete the control globally/i) ).toBeVisible(); const deleteRequest = mockedPage.waitForRequest( (request) => request.method() === 'DELETE' && - new RegExp(`/api/v1/controls/${deletedControlId}(\\?|$)`).test( - request.url() - ), + new RegExp( + `/api/v1/agents/[^/]+/controls/${deletedControlId}(\\?|$)` + ).test(request.url()), { timeout: 5000 } ); - await confirmModal.getByRole('button', { name: 'Delete' }).click(); + await confirmModal.getByRole('button', { name: 'Remove' }).click(); const request = await deleteRequest; - expect(request.url()).toContain(`/api/v1/controls/${deletedControlId}`); - expect(new URL(request.url()).searchParams.get('force')).toBe('true'); + expect(request.url()).toMatch( + new RegExp(`/api/v1/agents/[^/]+/controls/${deletedControlId}(\\?|$)`) + ); expect(request.method()).toBe('DELETE'); await expect(confirmModal).not.toBeVisible({ timeout: 5000 }); diff --git a/ui/tests/control-store.spec.ts b/ui/tests/control-store.spec.ts index 9d696a6c..8b48d0cc 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -48,7 +48,7 @@ test.describe('Control Store Modal', () => { ).toBeVisible(); // Status dot column has no header text, so we skip checking for it await expect( - modal.getByRole('columnheader', { name: 'Agent' }) + modal.getByRole('columnheader', { name: 'Used by' }) ).toBeVisible(); for (const control of mockData.listControls.controls) { @@ -58,19 +58,17 @@ test.describe('Control Store Modal', () => { } }); - test('displays agent links in Agent column', async ({ mockedPage }) => { + test('displays usage counts in Used by column', async ({ mockedPage }) => { const modal = await openControlStoreModal(mockedPage); - // PII Detection is used by Customer Support Bot - const agentLink = modal - .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' - ); + // Two controls are used by one agent each in fixture data + await expect(modal.getByText('1 agent')).toHaveCount(2); + // One control has no usage and renders as an em dash + await expect(modal.getByText('—')).toBeVisible(); + // Agent links are no longer rendered in this column + await expect( + modal.getByRole('link', { name: 'Customer Support Bot' }) + ).toHaveCount(0); }); test('can search for controls', async ({ mockedPage }) => { From c285d44c933bb9cbd4e684a780911d4948cc2805 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 17:11:41 -0500 Subject: [PATCH 08/32] fix: harden agent association semantics and add coverage --- sdks/python/src/agent_control/agents.py | 4 +- sdks/python/tests/test_agent_id_validation.py | 20 ++++- .../agent_control_server/endpoints/agents.py | 28 +++--- server/tests/test_agents_additional.py | 88 +++++++++++++++++++ 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index 7d76ffc9..4141addf 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -201,7 +201,9 @@ async def remove_agent_policy_association( """ Remove a specific policy association from an agent. - This operation is idempotent. + This operation is idempotent for existing agent/policy resources: + removing a non-associated link is a no-op. Missing agent/policy + resources still return 404. """ agent_id_str = ensure_uuid_str(agent_id) response = await client.http_client.delete( diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py index 0bcdeaa5..eb9ad525 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -1,10 +1,10 @@ """SDK agent_id validation behavior tests.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 +import agent_control import pytest - from agent_control import agents, policies @@ -88,3 +88,19 @@ async def test_get_agent_accepts_uuid_object() -> None: await agents.get_agent(client, agent_id) client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_id}") + + +@pytest.mark.asyncio +async def test_clear_agent_policies_calls_agents_module() -> None: + agent_id = uuid4() + + with patch( + "agent_control.__init__.agents.remove_agent_policies", new_callable=AsyncMock + ) as mock_remove: + mock_remove.return_value = {"success": True} + + result = await agent_control.clear_agent_policies(agent_id) + + assert result == {"success": True} + assert mock_remove.await_count == 1 + assert mock_remove.await_args.args[1] == agent_id diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index fb9f4539..4b2140fd 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -25,6 +25,7 @@ from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import BaseModel, ValidationError from sqlalchemy import delete, func, or_, select, union_all +from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -797,8 +798,6 @@ async def add_agent_policy( ) try: - from sqlalchemy.dialects.postgresql import insert as pg_insert - stmt = ( pg_insert(agent_policies) .values(agent_uuid=agent_id, policy_id=policy_id) @@ -852,7 +851,11 @@ async def get_agent_policies( async def remove_agent_policy( agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) ) -> AssocResponse: - """Remove a policy association from an agent (idempotent).""" + """Remove a policy association from an agent. + + Idempotent for existing resources: removing a non-associated link is a no-op. + Missing agent/policy resources still return 404. + """ await _get_agent_or_404(agent_id, db) policy_result = await db.execute(select(Policy.id).where(Policy.id == policy_id)) @@ -962,8 +965,6 @@ async def add_agent_control( ) try: - from sqlalchemy.dialects.postgresql import insert as pg_insert - stmt = ( pg_insert(agent_controls) .values(agent_uuid=agent_id, control_id=control_id) @@ -1012,23 +1013,16 @@ async def remove_agent_control( ) try: - direct_assoc_result = await db.execute( - select(agent_controls.c.control_id) + remove_direct_stmt = ( + delete(agent_controls) .where( (agent_controls.c.agent_uuid == agent_id) & (agent_controls.c.control_id == control_id) ) - .limit(1) + .returning(agent_controls.c.control_id) ) - removed_direct_association = direct_assoc_result.first() is not None - - if removed_direct_association: - await db.execute( - delete(agent_controls).where( - (agent_controls.c.agent_uuid == agent_id) - & (agent_controls.c.control_id == control_id) - ) - ) + remove_direct_result = await db.execute(remove_direct_stmt) + removed_direct_association = remove_direct_result.first() is not None # The control may still be active for this agent if inherited from policy association(s). policy_inheritance_result = await db.execute( diff --git a/server/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index fcca8e22..bf1e7f35 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -277,6 +277,39 @@ def test_set_agent_policy_incompatible_controls(client: TestClient) -> None: assert resp.json()["error_code"] == "POLICY_CONTROL_INCOMPATIBLE" +def test_add_agent_control_incompatible_with_agent_returns_400(client: TestClient) -> None: + # Given: Agent A exposes a custom evaluator + evaluators = [ + { + "name": "custom", + "description": "custom", + "config_schema": {"type": "object", "properties": {}, "additionalProperties": True}, + } + ] + _, agent_a_name = _init_agent(client, evaluators=evaluators) + + # And: control references Agent A's evaluator + control_payload = deepcopy(VALID_CONTROL_PAYLOAD) + control_payload["evaluator"] = { + "name": f"{agent_a_name}:custom", + "config": {}, + } + control_id = _create_control_with_data(client, control_payload) + + # And: Agent B does not expose that evaluator + agent_b_id, _ = _init_agent(client) + + # When: associating the control directly with Agent B + resp = client.post(f"/api/v1/agents/{agent_b_id}/controls/{control_id}") + + # Then: validation fails with compatibility error + assert resp.status_code == 400 + body = resp.json() + assert body["error_code"] == "POLICY_CONTROL_INCOMPATIBLE" + assert body["errors"] + assert all(err.get("field") == "evaluator" for err in body["errors"]) + + def test_init_agent_rejects_builtin_evaluator_name(client: TestClient) -> None: # Given: a payload that registers an evaluator matching a built-in name payload = { @@ -545,6 +578,61 @@ def test_delete_agent_policy_no_policy_assigned_is_idempotent(client: TestClient assert resp.json()["success"] is True +def test_remove_agent_policy_removes_only_target_association(client: TestClient) -> None: + # Given: an agent associated with two policies + agent_id, _ = _init_agent(client) + policy_a_id = _create_policy(client) + policy_b_id = _create_policy(client) + + assoc_a = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") + assert assoc_a.status_code == 200 + assoc_b = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") + assert assoc_b.status_code == 200 + + # When: removing one specific policy association + remove_resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") + + # Then: only that policy association is removed + assert remove_resp.status_code == 200 + assert remove_resp.json()["success"] is True + get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") + assert get_resp.status_code == 200 + assert get_resp.json()["policy_ids"] == [policy_b_id] + + +def test_remove_agent_policy_non_associated_existing_policy_is_noop(client: TestClient) -> None: + # Given: an agent associated with policy A, but policy B exists and is not associated + agent_id, _ = _init_agent(client) + policy_a_id = _create_policy(client) + policy_b_id = _create_policy(client) + + assoc_a = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") + assert assoc_a.status_code == 200 + + # When: removing non-associated policy B + remove_resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") + + # Then: operation succeeds and existing association remains + assert remove_resp.status_code == 200 + assert remove_resp.json()["success"] is True + get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") + assert get_resp.status_code == 200 + assert get_resp.json()["policy_ids"] == [policy_a_id] + + +def test_remove_agent_policy_missing_policy_returns_404(client: TestClient) -> None: + # Given: an existing agent and a non-existent policy id + agent_id, _ = _init_agent(client) + missing_policy_id = 999999999 + + # When: removing a missing policy association + resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{missing_policy_id}") + + # Then: policy not found error is returned + assert resp.status_code == 404 + assert resp.json()["error_code"] == "POLICY_NOT_FOUND" + + def test_list_agents_corrupted_data_sets_zero_counts(client: TestClient) -> None: # Given: an agent with corrupted data stored in the DB agent_id, _ = _init_agent( From 840a9dbb06287ccab909845f4c62939f3dd02026 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 17:34:45 -0500 Subject: [PATCH 09/32] fix: address review findings in server filters and control-store hooks --- ...t_policy_m2m_and_direct_agent_controls.py} | 4 +++ .../agent_control_server/endpoints/agents.py | 5 ++- .../endpoints/controls.py | 11 +++---- .../services/query_utils.py | 3 ++ server/tests/test_controls_additional.py | 18 +++++++++++ server/tests/test_init_agent.py | 32 +++++++++++++------ .../query-hooks/use-add-control-to-agent.ts | 8 +++-- .../modals/control-store/index.tsx | 3 +- 8 files changed, 64 insertions(+), 20 deletions(-) rename server/alembic/versions/{58e519e06e02_agent_policy_m2m_and_direct_agent_.py => 58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py} (89%) create mode 100644 server/src/agent_control_server/services/query_utils.py diff --git a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py similarity index 89% rename from server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py rename to server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py index e01d55a2..0e8f1549 100644 --- a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_.py +++ b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py @@ -54,6 +54,8 @@ def upgrade() -> None: def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + # NOTE: Downgrade can only restore one policy per agent via agents.policy_id. + # If multiple policies were associated to an agent, all but the minimum policy_id are lost. op.add_column('agents', sa.Column('policy_id', sa.INTEGER(), autoincrement=False, nullable=True)) op.execute( sa.text( @@ -74,6 +76,8 @@ def downgrade() -> None: op.drop_index(op.f('ix_agent_policies_policy_id'), table_name='agent_policies') op.drop_index(op.f('ix_agent_policies_agent_uuid'), table_name='agent_policies') op.drop_table('agent_policies') + # NOTE: Direct agent-control associations have no representation in the pre-upgrade schema. + # Dropping this table intentionally discards those associations during downgrade. op.drop_index(op.f('ix_agent_controls_control_id'), table_name='agent_controls') op.drop_index(op.f('ix_agent_controls_agent_uuid'), table_name='agent_controls') op.drop_table('agent_controls') diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 4b2140fd..b7131b69 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -51,6 +51,7 @@ parse_evaluator_ref_full, validate_config_against_schema, ) +from ..services.query_utils import escape_like_pattern from ..services.schema_compat import ( check_schema_compatibility, format_compatibility_error, @@ -181,7 +182,9 @@ async def list_agents( limit = min(max(1, limit), _MAX_PAGINATION_LIMIT) # Build base filter for name search - name_filter = Agent.name.ilike(f"%{name}%") if name else None + name_filter = ( + Agent.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") if name else None + ) # Get total count (with name filter if provided) count_query = select(func.count()).select_from(Agent) diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index 48a39973..4f274d05 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -37,6 +37,7 @@ parse_evaluator_ref_full, validate_config_against_schema, ) +from ..services.query_utils import escape_like_pattern # Pagination constants _DEFAULT_PAGINATION_LIMIT = 20 @@ -496,19 +497,15 @@ async def list_controls( Example: GET /controls?limit=10&enabled=true&step_type=tool """ - # Get total count (with filters applied) - count_query = select(func.count()).select_from(Control) query = select(Control).order_by(Control.id.desc()) # Apply cursor if cursor is not None: query = query.where(Control.id < cursor) - count_query = count_query.where(Control.id < cursor) # Apply name filter (case-insensitive partial match) if name is not None: - query = query.where(Control.name.ilike(f"%{name}%")) - # Don't apply to count_query - total should be pre-filter + query = query.where(Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\")) # Apply JSONB filters at database level if enabled is not None: @@ -554,7 +551,9 @@ async def list_controls( # Get total count (with same filters, but without cursor/limit) total_query = select(func.count()).select_from(Control) if name is not None: - total_query = total_query.where(Control.name.ilike(f"%{name}%")) + total_query = total_query.where( + Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") + ) if enabled is not None: if enabled: total_query = total_query.where( diff --git a/server/src/agent_control_server/services/query_utils.py b/server/src/agent_control_server/services/query_utils.py new file mode 100644 index 00000000..7e8e5376 --- /dev/null +++ b/server/src/agent_control_server/services/query_utils.py @@ -0,0 +1,3 @@ +def escape_like_pattern(value: str) -> str: + """Escape special SQL LIKE pattern characters in user-provided input.""" + return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index dc2fed69..177f5b2a 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -284,6 +284,24 @@ def test_list_controls_combined_filters(client: TestClient) -> None: assert names == [control1_name] +def test_list_controls_name_filter_treats_wildcards_as_literals(client: TestClient) -> None: + # Given: two controls where one only matches if '_' is interpreted as wildcard + literal_name = f"Control_{uuid.uuid4().hex[:6]}" + wildcard_match_name = literal_name.replace("_", "X", 1) + + _, created_literal_name = _create_control(client, name=literal_name) + _create_control(client, name=wildcard_match_name) + + # When: filtering by the literal name containing '_' + resp = client.get("/api/v1/controls", params={"name": literal_name}) + assert resp.status_code == 200 + names = [control["name"] for control in resp.json()["controls"]] + + # Then: only exact literal '_' matches are returned + assert names == [created_literal_name] + assert resp.json()["pagination"]["total"] == 1 + + def test_list_controls_enabled_true_includes_missing_enabled(client: TestClient) -> None: # Given: controls with enabled true, enabled false, and missing enabled control_true_id, control_true_name = _create_control(client, name=f"Enabled-{uuid.uuid4()}") diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index e03e35b5..655eec6e 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -1,17 +1,14 @@ -import json import logging import uuid from typing import Any -import pytest +from agent_control_server.config import db_config +from agent_control_server.models import Agent from fastapi import FastAPI from fastapi.testclient import TestClient -from sqlalchemy import create_engine, select, text +from sqlalchemy import create_engine, select from sqlalchemy.orm import Session -from agent_control_server.config import db_config -from agent_control_server.models import Agent - # Create sync engine for raw database queries in tests engine = create_engine(db_config.get_url(), echo=False) @@ -265,8 +262,6 @@ def test_init_agent_logs_warning_on_bad_existing_data(client: TestClient, caplog assert any("Failed to parse existing agent data" in m for m in messages) -import uuid - def _create_policy(client: TestClient) -> int: # Helper: create a policy via API and return id name = f"pol-{uuid.uuid4()}" @@ -417,7 +412,7 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: assert isinstance(body.get("controls"), list) # Verify control data is present and matches description assert any( - item.get("control", {}).get("description") == data_payload["description"] + item.get("control", {}).get("description") == data_payload["description"] for item in body["controls"] ) @@ -628,6 +623,25 @@ def test_list_agents_with_policy(client: TestClient) -> None: assert body["agents"][0]["policy_ids"] == [policy_id] +def test_list_agents_name_filter_treats_wildcards_as_literals(client: TestClient) -> None: + """Test name filter escapes SQL wildcard characters in user input.""" + literal_name = f"Agent_{uuid.uuid4().hex[:6]}" + wildcard_match_name = literal_name.replace("_", "X", 1) + + literal_payload = make_agent_payload(agent_id=str(uuid.uuid4()), name=literal_name) + wildcard_payload = make_agent_payload(agent_id=str(uuid.uuid4()), name=wildcard_match_name) + + assert client.post("/api/v1/agents/initAgent", json=literal_payload).status_code == 200 + assert client.post("/api/v1/agents/initAgent", json=wildcard_payload).status_code == 200 + + resp = client.get("/api/v1/agents", params={"name": literal_name}) + assert resp.status_code == 200 + names = [agent["agent_name"] for agent in resp.json()["agents"]] + + assert names == [literal_name] + assert resp.json()["pagination"]["total"] == 1 + + def test_list_agents_pagination(client: TestClient) -> None: """Test cursor-based pagination works correctly.""" # Given: 5 agents diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index a7a619dd..2cea8dbe 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -76,7 +76,11 @@ export function useAddControlToAgent() { } catch (error) { // Best effort cleanup: avoid orphan controls if a later step fails. if (createdControlId !== null) { - await api.controls.delete(createdControlId, { force: true }); + try { + await api.controls.delete(createdControlId, { force: true }); + } catch { + // Preserve the original error from the primary flow. + } } throw error; } @@ -86,7 +90,7 @@ export function useAddControlToAgent() { queryClient.invalidateQueries({ queryKey: ['controls'] }); queryClient.invalidateQueries({ queryKey: ['agent', variables.agentId] }); queryClient.invalidateQueries({ - queryKey: ['agentControls', variables.agentId], + queryKey: ['agent', variables.agentId, 'controls'], }); // Invalidate agents list query to refresh active controls count queryClient.invalidateQueries({ 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 175a68f3..4ad88ba7 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 @@ -165,8 +165,7 @@ export function ControlStoreModal({ }; loadControl(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editModalOpened, controlId, selectedControl]); + }, [editModalOpened, controlId, selectedControl, controls]); // Clear selectedControl when edit modal closes useEffect(() => { From 1b583e7e153523e02d4669f31b4923bcaf0413ca Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 17:47:06 -0500 Subject: [PATCH 10/32] chore: regenerate typescript sdk for updated agent policy docs --- sdks/typescript/src/generated/funcs/agents-remove-policy.ts | 5 ++++- sdks/typescript/src/generated/sdk/agents.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts index 7c13890f..2fb5dc92 100644 --- a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts @@ -31,7 +31,10 @@ import { Result } from "../types/fp.js"; * Remove policy association from agent * * @remarks - * Remove a policy association from an agent (idempotent). + * Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. */ export function agentsRemovePolicy( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index ad1dfff3..18414034 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -316,7 +316,10 @@ export class Agents extends ClientSDK { * Remove policy association from agent * * @remarks - * Remove a policy association from an agent (idempotent). + * Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. */ async removePolicy( request: From dad51181d6484c369d879086bb35806f42e3659c Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 23 Feb 2026 18:23:58 -0500 Subject: [PATCH 11/32] test: add behavioral coverage for policy/direct control invariants --- server/tests/test_agents_additional.py | 104 ++++++++++++++++++++++- server/tests/test_controls_additional.py | 67 +++++++++++++++ server/tests/test_evaluation_e2e.py | 52 +++++++++++- 3 files changed, 221 insertions(+), 2 deletions(-) diff --git a/server/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index bf1e7f35..25b9ff75 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -7,8 +7,8 @@ from fastapi.testclient import TestClient from sqlalchemy import text -from .utils import VALID_CONTROL_PAYLOAD from .conftest import engine +from .utils import VALID_CONTROL_PAYLOAD def _init_agent( @@ -767,6 +767,30 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: assert agent["active_controls_count"] == 2 +def test_list_agents_active_controls_count_deduplicates_policy_and_direct( + client: TestClient, +) -> None: + # Given: an agent with the same control linked through both policy and direct association + agent_id, _ = _init_agent(client) + policy_id = _create_policy(client) + shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) + + assoc_policy_control = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") + assert assoc_policy_control.status_code == 200 + assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assert assign_policy.status_code == 200 + assoc_direct_control = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") + assert assoc_direct_control.status_code == 200 + + # When: listing agents + resp = client.get("/api/v1/agents") + + # Then: active_controls_count counts the shared control once + assert resp.status_code == 200 + agent = next(a for a in resp.json()["agents"] if a["agent_id"] == agent_id) + assert agent["active_controls_count"] == 1 + + 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]}") @@ -874,6 +898,84 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> assert controls[0]["id"] == control_id +def test_init_agent_returns_union_and_deduplicates_policy_and_direct_controls( + client: TestClient, +) -> None: + # Given: an agent with policy-only, direct-only, and shared control associations + agent_id, agent_name = _init_agent(client) + policy_id = _create_policy(client) + + policy_only_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) + direct_only_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) + shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) + + assoc_policy_only = client.post( + f"/api/v1/policies/{policy_id}/controls/{policy_only_control_id}" + ) + assert assoc_policy_only.status_code == 200 + assoc_shared_policy = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") + assert assoc_shared_policy.status_code == 200 + assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assert assign_policy.status_code == 200 + + assoc_direct_only = client.post(f"/api/v1/agents/{agent_id}/controls/{direct_only_control_id}") + assert assoc_direct_only.status_code == 200 + assoc_shared_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") + assert assoc_shared_direct.status_code == 200 + + # When: re-initializing the same agent + reinit_resp = client.post( + "/api/v1/agents/initAgent", + json={ + "agent": { + "agent_id": agent_id, + "agent_name": agent_name, + "agent_description": "desc", + "agent_version": "1.0", + }, + "steps": [], + "evaluators": [], + }, + ) + + # Then: initAgent returns the union of controls with shared control de-duplicated + assert reinit_resp.status_code == 200 + body = reinit_resp.json() + assert body["created"] is False + returned_control_ids = [control["id"] for control in body["controls"]] + assert set(returned_control_ids) == { + policy_only_control_id, + direct_only_control_id, + shared_control_id, + } + assert len(returned_control_ids) == 3 + + +def test_policy_removal_does_not_remove_direct_association_for_same_control( + client: TestClient, +) -> None: + # Given: an agent where the same control is linked both via policy and directly + agent_id, _ = _init_agent(client) + policy_id = _create_policy(client) + shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) + + assoc_control = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") + assert assoc_control.status_code == 200 + assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assert assign_policy.status_code == 200 + assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") + assert assoc_direct.status_code == 200 + + # When: removing the policy association from the agent + remove_policy = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + + # Then: the control is still active through the direct association + assert remove_policy.status_code == 200 + list_controls = client.get(f"/api/v1/agents/{agent_id}/controls") + assert list_controls.status_code == 200 + assert {control["id"] for control in list_controls.json()["controls"]} == {shared_control_id} + + def test_patch_agent_corrupted_data_returns_422(client: TestClient) -> None: # Given: an agent with corrupted stored data agent_id, _ = _init_agent(client) diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 177f5b2a..1a944fed 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -460,6 +460,73 @@ def test_delete_control_force_dissociates(client: TestClient) -> None: assert control_id not in list_resp.json()["control_ids"] +def test_delete_control_force_dissociates_from_policy_and_direct(client: TestClient) -> None: + # Given: one control associated both to a policy and directly to an agent + control_id, _ = _create_control(client) + _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) + + policy_resp = client.put("/api/v1/policies", json={"name": f"policy-{uuid.uuid4()}"}) + assert policy_resp.status_code == 200 + policy_id = policy_resp.json()["policy_id"] + assoc_policy = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") + assert assoc_policy.status_code == 200 + + agent_id = _init_agent(client, name=f"agent-{uuid.uuid4()}") + assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") + assert assoc_direct.status_code == 200 + + # When: deleting with force + delete_resp = client.delete(f"/api/v1/controls/{control_id}?force=true") + + # Then: response reports dissociation from both paths + assert delete_resp.status_code == 200 + body = delete_resp.json() + assert body["success"] is True + assert body["dissociated_from_policies"] == [policy_id] + assert body["dissociated_from_agents"] == [agent_id] + + # Then: policy and agent no longer expose the control + list_policy_controls = client.get(f"/api/v1/policies/{policy_id}/controls") + assert list_policy_controls.status_code == 200 + assert control_id not in list_policy_controls.json()["control_ids"] + + list_agent_controls = client.get(f"/api/v1/agents/{agent_id}/controls") + assert list_agent_controls.status_code == 200 + assert control_id not in {control["id"] for control in list_agent_controls.json()["controls"]} + + # Then: the control resource is deleted globally + get_control = client.get(f"/api/v1/controls/{control_id}") + assert get_control.status_code == 404 + + +def test_delete_control_without_force_blocked_by_direct_association( + client: TestClient, +) -> None: + # Given: a control directly associated to an agent + control_id, _ = _create_control(client) + _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) + + agent_id = _init_agent(client, name=f"agent-{uuid.uuid4()}") + assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") + assert assoc_direct.status_code == 200 + + # When: deleting without force + delete_resp = client.delete(f"/api/v1/controls/{control_id}") + + # Then: delete is rejected as control-in-use by agent association + assert delete_resp.status_code == 409 + body = delete_resp.json() + assert body["error_code"] == "CONTROL_IN_USE" + assert any( + err.get("resource") == "Agent" + and ( + err.get("value") == agent_id + or agent_id in err.get("message", "") + ) + for err in body.get("errors", []) + ) + + def test_get_control_corrupted_data_returns_none(client: TestClient) -> None: # Given: a control with corrupted data in DB control_id, control_name = _create_control(client) diff --git a/server/tests/test_evaluation_e2e.py b/server/tests/test_evaluation_e2e.py index bea4b961..66a4e206 100644 --- a/server/tests/test_evaluation_e2e.py +++ b/server/tests/test_evaluation_e2e.py @@ -1,7 +1,9 @@ """End-to-end tests for evaluation flow.""" import uuid -from fastapi.testclient import TestClient + from agent_control_models import EvaluationRequest, Step +from fastapi.testclient import TestClient + from .utils import create_and_assign_policy @@ -61,6 +63,54 @@ def test_evaluation_no_policy(client: TestClient): assert not resp.json()["matches"] +def test_evaluation_uses_direct_controls_without_policy(client: TestClient): + # Given: an agent with no policy and a direct deny control attached + agent_uuid = uuid.uuid4() + init_resp = client.post( + "/api/v1/agents/initAgent", + json={ + "agent": {"agent_id": str(agent_uuid), "agent_name": "DirectControlAgent"}, + "steps": [], + }, + ) + assert init_resp.status_code == 200 + + control_name = f"control-{uuid.uuid4()}" + create_control = client.put("/api/v1/controls", json={"name": control_name}) + assert create_control.status_code == 200 + control_id = create_control.json()["control_id"] + + control_data = { + "description": "Direct deny control", + "enabled": True, + "execution": "server", + "scope": {"step_types": ["llm"], "stages": ["pre"]}, + "selector": {"path": "input"}, + "evaluator": {"name": "regex", "config": {"pattern": "secret"}}, + "action": {"decision": "deny"}, + } + set_data = client.put(f"/api/v1/controls/{control_id}/data", json={"data": control_data}) + assert set_data.status_code == 200 + + add_direct = client.post(f"/api/v1/agents/{str(agent_uuid)}/controls/{control_id}") + assert add_direct.status_code == 200 + + # When: evaluating input that matches the direct control + req = EvaluationRequest( + agent_uuid=agent_uuid, + step=Step(type="llm", name="test-step", input="contains a secret", output=None), + stage="pre", + ) + eval_resp = client.post("/api/v1/evaluation", json=req.model_dump(mode="json")) + + # Then: evaluation is denied by the direct control even without any policy + assert eval_resp.status_code == 200 + body = eval_resp.json() + assert body["is_safe"] is False + assert body["matches"] is not None + assert any(match["control_name"] == control_name for match in body["matches"]) + + def test_evaluation_empty_policy(client: TestClient): """Test that an agent with an empty policy is safe.""" # Given: an empty policy From 44088a996f35de8485c0a6b0a51f40b03233577c Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Tue, 24 Feb 2026 14:32:20 -0500 Subject: [PATCH 12/32] update_readme --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 92e40cb1..27216ff7 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,44 @@ async def main(): > **Note**: Authentication is disabled by default for local development. See [docs/REFERENCE.md](docs/REFERENCE.md#authentication) for production setup. +### 5. Assign Controls and Policies + +Controls can be associated with agents in two ways — directly, via policies, or both. An agent's **active controls** are the union of all direct controls and all controls inherited from associated policies. + +**Direct controls** — attach individual controls to an agent: + +```bash +# Add a control directly to an agent +curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/controls/3 + +# Remove a direct control +curl -X DELETE http://localhost:8000/api/v1/agents/support-agent-v1/controls/3 +``` + +**Policies** — group controls and assign them to one or more agents: + +```bash +# Associate a policy with an agent (agents can have multiple policies) +curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/policies/1 + +# Add another policy +curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/policies/2 + +# List all policies for an agent +curl http://localhost:8000/api/v1/agents/support-agent-v1/policies + +# Remove a specific policy +curl -X DELETE http://localhost:8000/api/v1/agents/support-agent-v1/policies/1 +``` + +**List all active controls** (union of direct + policy-inherited): + +```bash +curl http://localhost:8000/api/v1/agents/support-agent-v1/controls +``` + +> Both policies and direct controls are optional. An agent can operate with no controls, only direct controls, only policies, or any combination. + --- ## Configuration @@ -225,6 +263,7 @@ Agent Control is built as a monorepo with these components: ▼ ┌──────────────────────────────────────────────────────────────────┐ │ Agent Control Server │ +│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Controls │ │ Policies │ │ Evaluators │ │ Agents │ │ │ │ API │ │ API │ │ Registry │ │ API │ │ @@ -244,6 +283,7 @@ Agent Control is built as a monorepo with these components: | Package | Description | |:--------|:------------| | `agent-control-sdk` | Python SDK with `@control()` decorator | +| `agent-control` (npm) | TypeScript SDK (generated from OpenAPI) | | `agent-control-server` | FastAPI server with Control Management API | | `agent-control-engine` | Core evaluation logic and evaluator system | | `agent-control-models` | Shared Pydantic v2 models | @@ -259,10 +299,12 @@ Agent Control is built as a monorepo with these components: ``` agent-control/ ├── sdks/python/ # Python SDK (agent-control) +├── sdks/typescript/ # TypeScript SDK (generated) ├── server/ # FastAPI server (agent-control-server) ├── engine/ # Evaluation engine (agent-control-engine) ├── models/ # Shared models (agent-control-models) ├── evaluators/ # Evaluator implementations (agent-control-evaluators) +├── ui/ # Next.js web dashboard └── examples/ # Usage examples ``` From eaf6ba63bd284d5a3f6e5d58e2cd0d766f117482 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Wed, 25 Feb 2026 12:17:30 -0500 Subject: [PATCH 13/32] docs(ui): remove policy wording from docs and user-facing UI copy --- README.md | 30 +++---------- docs/OVERVIEW.md | 21 ++++----- docs/REFERENCE.md | 45 +++++-------------- docs/evaluators/sql.md | 4 +- ui/README.md | 2 +- .../controls/use-delete-control-flow.tsx | 6 +-- 6 files changed, 33 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 27216ff7..743b996f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ **Runtime guardrails for AI agents — configurable, extensible, and production-ready.** -AI agents interact with users, tools, and external systems in unpredictable ways. **Agent Control** provides an extensible, policy-based runtime layer that evaluates inputs and outputs against configurable rules — blocking prompt injections, PII leakage, and other risks without modifying your agent's code. +AI agents interact with users, tools, and external systems in unpredictable ways. **Agent Control** provides an extensible, control-based runtime layer that evaluates inputs and outputs against configurable rules — blocking prompt injections, PII leakage, and other risks without modifying your agent's code. ![Agent Control Architecture](docs/images/Architecture.png) @@ -36,7 +36,7 @@ See the [Concepts guide](CONCEPTS.md) for a deep dive into Agent Control's archi - **Safety Without Code Changes** — Add guardrails with a `@control()` decorator - **Runtime Configuration** — Update controls without redeploying your application -- **Centralized Policies** — Define controls once, apply to multiple agents +- **Centralized Controls** — Define controls once, apply to multiple agents - **Web Dashboard** — Manage agents and controls through the UI - **API Key Authentication** — Secure your control server in production - **Pluggable Evaluators** — Regex, list matching, AI-powered detection (Luna-2), or custom evaluators @@ -132,9 +132,9 @@ async def main(): > **Note**: Authentication is disabled by default for local development. See [docs/REFERENCE.md](docs/REFERENCE.md#authentication) for production setup. -### 5. Assign Controls and Policies +### 5. Assign Controls -Controls can be associated with agents in two ways — directly, via policies, or both. An agent's **active controls** are the union of all direct controls and all controls inherited from associated policies. +Controls can be associated directly with agents. An agent's **active controls** are the controls currently linked to that agent. **Direct controls** — attach individual controls to an agent: @@ -146,29 +146,13 @@ curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/controls/3 curl -X DELETE http://localhost:8000/api/v1/agents/support-agent-v1/controls/3 ``` -**Policies** — group controls and assign them to one or more agents: - -```bash -# Associate a policy with an agent (agents can have multiple policies) -curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/policies/1 - -# Add another policy -curl -X POST http://localhost:8000/api/v1/agents/support-agent-v1/policies/2 - -# List all policies for an agent -curl http://localhost:8000/api/v1/agents/support-agent-v1/policies - -# Remove a specific policy -curl -X DELETE http://localhost:8000/api/v1/agents/support-agent-v1/policies/1 -``` - -**List all active controls** (union of direct + policy-inherited): +**List all active controls**: ```bash curl http://localhost:8000/api/v1/agents/support-agent-v1/controls ``` -> Both policies and direct controls are optional. An agent can operate with no controls, only direct controls, only policies, or any combination. +> Controls are optional. An agent can operate with no controls configured. --- @@ -265,7 +249,7 @@ Agent Control is built as a monorepo with these components: │ Agent Control Server │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -│ │ Controls │ │ Policies │ │ Evaluators │ │ Agents │ │ +│ │ Controls │ │Control Link│ │ Evaluators │ │ Agents │ │ │ │ API │ │ API │ │ Registry │ │ API │ │ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ └──────────────────────────────────────────────────────────────────┘ diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index c162b335..4cd3270f 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -2,7 +2,7 @@ **Runtime guardrails for AI agents — configurable, extensible, and production-ready.** -Agent Control provides a policy-based control layer that sits between your AI agents and the outside world. It evaluates inputs and outputs against configurable rules, blocking harmful content, prompt injections, PII leakage, and other risks — all without changing your agent's code. +Agent Control provides a control-based runtime layer that sits between your AI agents and the outside world. It evaluates inputs and outputs against configurable rules, blocking harmful content, prompt injections, PII leakage, and other risks — all without changing your agent's code. --- @@ -57,15 +57,12 @@ A **Control Set** is a named group of related controls. Use them to organize con | `compliance-controls` | block-pii, block-phi, audit-logging | | `quality-controls` | check-hallucination, verify-sources | -### 📜 Policies +### 🔗 Control Associations -A **Policy** combines one or more Control Sets and is assigned to agents. Policies let you: -- Reuse control sets across multiple agents -- Version and audit your safety rules -- Apply different policies to different environments (dev/staging/prod) +Controls can be linked directly to agents and reused across multiple agents. This keeps rollout and tuning fast without redeploying application code. ``` -Policy → Control Sets → Controls → Agents +Controls → Agents ``` ### 🎯 Selectors @@ -334,14 +331,14 @@ async def chat(message: str) -> str: Update controls without redeploying your application. Critical for: - Responding to emerging threats - Tuning thresholds based on real-world data -- A/B testing different safety policies +- A/B testing different safety controls -### 🎯 Centralized Policy Management -Define controls once, apply them to multiple agents. Security teams can manage policies independently from development teams. +### 🎯 Centralized Control Management +Define controls once, apply them to multiple agents. Security teams can manage controls independently from development teams. ``` ┌─────────────────────────────────────────────────────┐ -│ Policy │ +│ Control Collection │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Control Set │ │ Control Set │ │ Control Set │ │ │ │ (Safety) │ │ (Compliance)│ │ (Quality) │ │ @@ -397,7 +394,7 @@ Choose how to handle failures: ┌──────────────────────────────────────────────────────────────────┐ │ Agent Control Server │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ -│ │ Controls │ │ Policies │ │ Evaluators │ │ Agents │ │ +│ │ Controls │ │Control Link│ │ Evaluators │ │ Agents │ │ │ │ API │ │ API │ │ Registry │ │ API │ │ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ └──────────────────────────────────────────────────────────────────┘ diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 3f54ccea..b737c8fc 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -20,7 +20,7 @@ This document provides comprehensive technical reference for Agent Control. Each ## Introduction -Agent Control provides a policy-based control layer that sits between your AI agents and the outside world. It evaluates inputs and outputs against configurable rules, blocking harmful content, prompt injections, PII leakage, and other risks. +Agent Control provides a control-based runtime layer that sits between your AI agents and the outside world. It evaluates inputs and outputs against configurable rules, blocking harmful content, prompt injections, PII leakage, and other risks. ### Why Agent Control? @@ -64,16 +64,12 @@ Example: *"If the output contains an SSN pattern, block the response."* } ``` -### Policies +### Control Associations -A **Policy** is a named collection of controls assigned to agents. Policies enable you to: - -- Reuse control sets across multiple agents -- Version and audit your safety rules -- Apply different policies to different environments (dev/staging/prod) +Controls can be assigned directly to agents and reused across multiple agents. ``` -Policy → Controls → Agents +Controls → Agents ``` ### Check Stages @@ -248,7 +244,7 @@ graph TB ### Agent Initialization 1. **Agent Registration**: Agent initializes with `agent_control.init()`, registering with the server -2. **Policy Assignment**: Server returns the agent's assigned policy and active controls +2. **Control Resolution**: Server returns the agent's active controls ### Control Execution Flow 1. **Function Invocation**: User calls a function decorated with `@control()` @@ -602,7 +598,7 @@ agent_control.init( ### The @control Decorator -The `@control()` decorator applies server-side policies to any function. +The `@control()` decorator applies server-side controls to any function. ```python from agent_control import control @@ -624,7 +620,6 @@ async def chat(message: str) -> str: **Parameters**: -- `policy` (str, optional): Policy name for documentation purposes. The agent's assigned policy is automatically used. - `step_name` (str, optional): Custom name for this step. If not provided, uses the function name. Useful for: - Overriding auto-detected names when they don't match your control configuration - Applying the same controls to functions with different names @@ -741,22 +736,14 @@ async with AgentControlClient() as client: | `agent_control.get_control()` | Get control by ID | | `agent_control.update_control()` | Update control properties | | `agent_control.delete_control()` | Delete a control | -| `agent_control.add_control_to_policy()` | Add control to policy | -| `agent_control.remove_control_from_policy()` | Remove control from policy | -| `agent_control.list_policy_controls()` | List controls in a policy | - -**Policy management** (via `agent_control.policies` module): - -| Function | Description | -|----------|-------------| -| `policies.create_policy()` | Create a new policy | -| `policies.assign_policy_to_agent()` | Assign policy to agent | +| `agent_control.add_control_to_agent()` | Add control to agent | +| `agent_control.remove_control_from_agent()` | Remove control from agent | --- ## Server API -The Agent Control server exposes a RESTful API for managing agents, controls, and policies. +The Agent Control server exposes a RESTful API for managing agents and controls. ### Base URL @@ -773,9 +760,8 @@ Default: `http://localhost:8000/api/v1` | `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 | +| `POST` | `/agents/{agent_id}/controls/{control_id}` | Add control to agent | +| `DELETE` | `/agents/{agent_id}/controls/{control_id}` | Remove control from agent | **Controls**: @@ -787,15 +773,6 @@ Default: `http://localhost:8000/api/v1` | `PATCH` | `/controls/{control_id}` | Update control | | `DELETE` | `/controls/{control_id}` | Delete control | -**Policies**: - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `PUT` | `/policies` | Create policy | -| `GET` | `/policies/{policy_id}/controls` | List controls in policy | -| `POST` | `/policies/{policy_id}/controls/{control_id}` | Add control to policy | -| `DELETE` | `/policies/{policy_id}/controls/{control_id}` | Remove control | - **System**: | Method | Endpoint | Description | diff --git a/docs/evaluators/sql.md b/docs/evaluators/sql.md index 5ca18ae3..74fe2b77 100644 --- a/docs/evaluators/sql.md +++ b/docs/evaluators/sql.md @@ -6,7 +6,7 @@ A practical guide for configuring SQL validation controls in your AI agent. ## What is the SQL Evaluator? -The SQL Evaluator validates SQL query strings (e.g., from LLM responses) before they execute against your database. It acts as a security and safety layer, preventing dangerous operations, enforcing access policies, and ensuring data isolation. +The SQL Evaluator validates SQL query strings (e.g., from LLM responses) before they execute against your database. It acts as a security and safety layer, preventing dangerous operations, enforcing access rules, and ensuring data isolation. **Technical Foundation**: Uses [sqlglot](https://github.com/tobymao/sqlglot) with the Rust-accelerated parser (`sqlglot[rs]`) for high-performance SQL parsing and AST-based validation. @@ -36,7 +36,7 @@ The SQL Evaluator validates SQL query strings (e.g., from LLM responses) before > **⚠️ Important Security Note** > -> This evaluator validates query structure and enforces access rules, but **it is not a complete defense against SQL injection**. The primary defense against SQL injection is using **prepared statements** (parameterized queries) at the database layer. This evaluator provides an additional security layer by validating query syntax and enforcing policies, but should not be relied upon as the sole protection mechanism. +> This evaluator validates query structure and enforces access rules, but **it is not a complete defense against SQL injection**. The primary defense against SQL injection is using **prepared statements** (parameterized queries) at the database layer. This evaluator provides an additional security layer by validating query syntax and enforcing controls, but should not be relied upon as the sole protection mechanism. > > **Best Practice**: Always use prepared statements/parameterized queries when executing SQL from untrusted sources. This evaluator complements, but does not replace, proper database security practices. diff --git a/ui/README.md b/ui/README.md index a6395b07..aaabc41d 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,6 +1,6 @@ # Agent Control UI -Next.js dashboard for managing [Agent Control](https://github.com/agentcontrol/agent-control) agents, controls, and policies. Runs on port 4000. +Next.js dashboard for managing [Agent Control](https://github.com/agentcontrol/agent-control) agents and controls. Runs on port 4000. ## Prerequisites diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 5b0c3efa..2e94ab3e 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -51,8 +51,8 @@ export function useDeleteControlFlow({ if (!removedDirect) { notifications.show({ - title: 'Control inherited from policy', - message: `"${control.name}" has no direct link on this agent. Remove it from policy to disable it.`, + title: 'Control is linked indirectly', + message: `"${control.name}" has no direct link on this agent. Remove its inherited link to disable it.`, color: 'yellow', }); return; @@ -63,7 +63,7 @@ export function useDeleteControlFlow({ ? 'Direct association removed' : 'Control removed', message: stillActive - ? `"${control.name}" is still active through policy inheritance.` + ? `"${control.name}" is still active through another inherited link.` : `"${control.name}" has been removed from this agent.`, color: 'green', }); From c012721964aaa7d954e1498cf7da9836ca79bb2f Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Thu, 26 Feb 2026 17:39:13 -0500 Subject: [PATCH 14/32] fix(server): remove stale agent.policy_id check --- server/src/agent_control_server/endpoints/agents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 2a98cdaf..e479aedd 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -176,7 +176,7 @@ async def _build_overwrite_evaluator_removals( db: AsyncSession, ) -> list[InitAgentEvaluatorRemoval]: """Build evaluator removal details, including active-control references.""" - if not removed_evaluators or agent.policy_id is None: + if not removed_evaluators: return [InitAgentEvaluatorRemoval(name=name) for name in sorted(removed_evaluators)] try: From f4245c68dbf91cf6410c9a435c99b90b8266a425 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Thu, 26 Feb 2026 17:51:16 -0500 Subject: [PATCH 15/32] test(server): use policies route in overwrite conflict test --- server/tests/test_init_agent_conflict_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/tests/test_init_agent_conflict_mode.py b/server/tests/test_init_agent_conflict_mode.py index f8efa3eb..b68bdaa3 100644 --- a/server/tests/test_init_agent_conflict_mode.py +++ b/server/tests/test_init_agent_conflict_mode.py @@ -180,7 +180,7 @@ def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: Test policy_id, control_id, control_name = _create_policy_with_agent_evaluator_control( client, agent_name=agent_name, evaluator_name=evaluator_name ) - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") assert assign_resp.status_code == 200 # When: overwrite mode removes the evaluator from the incoming registration payload. From 7ee3d48048dae6bc4c5bee020f84740c28936945 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Thu, 26 Feb 2026 18:02:30 -0500 Subject: [PATCH 16/32] fix(server): rebase agent policy m2m migration to squashed schema --- ...e519e06e02_agent_policy_m2m_and_direct_agent_controls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py index 0e8f1549..a106f2ae 100644 --- a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py +++ b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py @@ -1,6 +1,6 @@ """ Revision ID: 58e519e06e02 -Revises: d2f4a6b8c9d0 +Revises: 53d8427083e2 Create Date: 2026-02-23 13:39:21.295377 """ @@ -9,8 +9,8 @@ # revision identifiers, used by Alembic. -revision = '58e519e06e02' -down_revision = 'd2f4a6b8c9d0' +revision = "58e519e06e02" +down_revision = "53d8427083e2" branch_labels = None depends_on = None From e5240c75b9384c8873ae836e174ff4cb622474ea Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 13:25:47 -0800 Subject: [PATCH 17/32] fix: repair post-merge test and typing regressions --- engine/tests/test_core.py | 9 +++++---- sdks/python/src/agent_control/agents.py | 1 + sdks/python/src/agent_control/evaluation.py | 1 + sdks/python/tests/test_integration_agents.py | 4 ++-- sdks/python/tests/test_local_evaluation.py | 4 ++-- server/tests/test_observability_store_postgres.py | 10 +++++----- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/engine/tests/test_core.py b/engine/tests/test_core.py index 7799ded4..8ae910ab 100644 --- a/engine/tests/test_core.py +++ b/engine/tests/test_core.py @@ -9,6 +9,7 @@ import asyncio from dataclasses import dataclass from typing import Any +from uuid import uuid4 import pytest from agent_control_engine import clear_evaluator_cache @@ -1623,7 +1624,7 @@ async def test_steer_control_error_non_blocking(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_name="test-agent", + agent_uuid=uuid4(), stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1682,7 +1683,7 @@ async def test_deny_control_error_blocks(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_name="test-agent", + agent_uuid=uuid4(), stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1731,7 +1732,7 @@ async def test_mixed_deny_and_steer_errors(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_name="test-agent", + agent_uuid=uuid4(), stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1826,7 +1827,7 @@ class MockControl: engine = ControlEngine(controls) request = EvaluationRequest( - agent_name="test-agent", + agent_uuid=uuid4(), stage="pre", step=Step( type="llm", diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index 8c37dc49..27b0a122 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -10,6 +10,7 @@ from .client import AgentControlClient from .validation import ensure_uuid_str + async def register_agent( client: AgentControlClient, agent: Agent, diff --git a/sdks/python/src/agent_control/evaluation.py b/sdks/python/src/agent_control/evaluation.py index deddd6ee..235b8000 100644 --- a/sdks/python/src/agent_control/evaluation.py +++ b/sdks/python/src/agent_control/evaluation.py @@ -399,6 +399,7 @@ async def check_evaluation_with_local( confidence=0.0, error=f"Failed to parse local control: {e}", ), + steering_context=None, ) ) diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index bbd7c529..6a2de95b 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -147,7 +147,7 @@ async def test_list_agent_controls_typed_returns_model( # WHEN: controls are requested via the typed API wrapper. response = await agent_control.agents.list_agent_controls_typed( client, - test_agent["agent_name"], + test_agent["agent_id"], ) # THEN: a typed model is returned. @@ -166,7 +166,7 @@ async def test_list_agent_controls_returns_dict_payload( # WHEN: controls are requested via the dict API wrapper. response = await agent_control.agents.list_agent_controls( client, - test_agent["agent_name"], + test_agent["agent_id"], ) # THEN: a dict payload is returned with controls list. diff --git a/sdks/python/tests/test_local_evaluation.py b/sdks/python/tests/test_local_evaluation.py index 6cf19a89..78c0d657 100644 --- a/sdks/python/tests/test_local_evaluation.py +++ b/sdks/python/tests/test_local_evaluation.py @@ -748,7 +748,7 @@ async def test_malformed_server_control_still_calls_server(self, agent_uuid, llm assert result.is_safe is True @pytest.mark.asyncio - async def test_local_evaluation_includes_steering_context(self, agent_name, llm_payload): + async def test_local_evaluation_includes_steering_context(self, agent_uuid, llm_payload): """Test that local evaluation includes steering_context in response. Given: A local steer control with steering_context configured @@ -785,7 +785,7 @@ async def test_local_evaluation_includes_steering_context(self, agent_name, llm_ result = await check_evaluation_with_local( client=client, - agent_name=agent_name, + agent_uuid=agent_uuid, step=llm_payload, stage="pre", controls=controls, diff --git a/server/tests/test_observability_store_postgres.py b/server/tests/test_observability_store_postgres.py index 3cb1e205..338bdb48 100644 --- a/server/tests/test_observability_store_postgres.py +++ b/server/tests/test_observability_store_postgres.py @@ -219,12 +219,12 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() ) store = PostgresEventStore(session_maker) - agent_name = f"agent-{uuid4().hex[:12]}" + agent_uuid = uuid4() now = datetime.now(UTC) events = [ _event( - agent_name=agent_name, + agent_uuid=agent_uuid, control_id=1, action="steer", matched=True, @@ -232,7 +232,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() trace_id="a" * 32, ), _event( - agent_name=agent_name, + agent_uuid=agent_uuid, control_id=2, action="warn", matched=True, @@ -240,7 +240,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() trace_id="b" * 32, ), _event( - agent_name=agent_name, + agent_uuid=agent_uuid, control_id=3, action="allow", matched=True, @@ -254,7 +254,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() # When: querying stats with timeseries enabled stats = await store.query_stats( - agent_name, + agent_uuid, time_range=timedelta(hours=1), include_timeseries=True, bucket_size=timedelta(minutes=1), From eec9cbd3b49c9392747ad6538394c05f0702d13c Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 14:43:39 -0800 Subject: [PATCH 18/32] fix: restore name-native agent identity from main --- engine/tests/test_core.py | 73 +- models/src/agent_control_models/agent.py | 36 +- models/src/agent_control_models/errors.py | 14 +- models/src/agent_control_models/evaluation.py | 27 +- .../src/agent_control_models/observability.py | 83 +- models/src/agent_control_models/server.py | 54 +- sdks/python/src/agent_control/__init__.py | 169 +--- sdks/python/src/agent_control/agents.py | 228 +----- .../src/agent_control/control_decorators.py | 29 +- sdks/python/src/agent_control/controls.py | 11 +- sdks/python/src/agent_control/evaluation.py | 276 ++----- sdks/python/src/agent_control/policies.py | 15 +- sdks/python/src/agent_control/validation.py | 28 +- sdks/python/tests/README.md | 4 +- sdks/python/tests/conftest.py | 13 +- sdks/python/tests/test_agent_id_validation.py | 80 +- sdks/python/tests/test_control_decorators.py | 38 +- sdks/python/tests/test_evaluation.py | 6 +- sdks/python/tests/test_init_conflict.py | 5 +- sdks/python/tests/test_init_step_merge.py | 18 +- sdks/python/tests/test_init_validation.py | 7 +- sdks/python/tests/test_integration_agents.py | 28 +- sdks/python/tests/test_local_evaluation.py | 71 +- sdks/python/tests/test_observability.py | 2 +- .../tests/test_observability_updates.py | 17 +- sdks/python/tests/test_policy_refresh_loop.py | 11 - sdks/python/tests/test_validation.py | 42 +- ...7fe_name_native_agent_identity_hard_cut.py | 52 ++ ...nt_policy_m2m_and_direct_agent_controls.py | 84 -- .../agent_control_server/endpoints/agents.py | 748 +++++++----------- .../endpoints/controls.py | 144 ++-- .../endpoints/evaluation.py | 11 +- .../endpoints/evaluators.py | 2 +- .../endpoints/observability.py | 22 +- server/src/agent_control_server/errors.py | 4 +- server/src/agent_control_server/main.py | 2 +- server/src/agent_control_server/models.py | 65 +- .../observability/ingest/direct.py | 1 - .../observability/store/base.py | 5 +- .../observability/store/postgres.py | 31 +- .../services/agent_names.py | 35 + .../agent_control_server/services/controls.py | 36 +- .../services/schema_compat.py | 2 +- server/tests/test_agents_additional.py | 357 +++------ server/tests/test_controls_additional.py | 202 ++--- server/tests/test_error_handling.py | 37 +- server/tests/test_evaluation_e2e.py | 112 +-- .../test_evaluation_e2e_list_evaluator.py | 68 +- .../test_evaluation_e2e_sql_evaluator.py | 80 +- .../tests/test_evaluation_error_handling.py | 18 +- server/tests/test_evaluator_schemas.py | 15 +- server/tests/test_init_agent.py | 309 +++----- server/tests/test_init_agent_conflict_mode.py | 19 +- server/tests/test_init_agent_force_replace.py | 18 +- server/tests/test_new_features.py | 49 +- .../tests/test_observability_direct_ingest.py | 10 +- server/tests/test_observability_endpoints.py | 111 +-- server/tests/test_observability_models.py | 31 +- .../test_observability_store_postgres.py | 42 +- server/tests/test_policy_integration.py | 53 +- server/tests/test_services_controls.py | 45 +- server/tests/utils.py | 17 +- 62 files changed, 1595 insertions(+), 2627 deletions(-) create mode 100644 server/alembic/versions/58920e6807fe_name_native_agent_identity_hard_cut.py delete mode 100644 server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py create mode 100644 server/src/agent_control_server/services/agent_names.py diff --git a/engine/tests/test_core.py b/engine/tests/test_core.py index 8ae910ab..d5e7418f 100644 --- a/engine/tests/test_core.py +++ b/engine/tests/test_core.py @@ -9,7 +9,6 @@ import asyncio from dataclasses import dataclass from typing import Any -from uuid import uuid4 import pytest from agent_control_engine import clear_evaluator_cache @@ -246,7 +245,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", ) @@ -278,7 +277,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", ) @@ -313,7 +312,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", ) @@ -348,7 +347,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", ) @@ -374,7 +373,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", ) @@ -400,7 +399,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", ) @@ -434,7 +433,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", ) @@ -457,7 +456,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", ) @@ -545,7 +544,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", ) @@ -581,7 +580,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", ) @@ -619,7 +618,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", ) @@ -651,7 +650,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", ) @@ -688,7 +687,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", ) @@ -734,7 +733,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", ) @@ -775,7 +774,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", ) @@ -809,7 +808,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", ) @@ -841,7 +840,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", ) @@ -870,7 +869,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", ) @@ -959,7 +958,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", ) @@ -996,7 +995,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", ) @@ -1024,7 +1023,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", ) @@ -1049,7 +1048,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", ) @@ -1114,7 +1113,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", ) @@ -1172,7 +1171,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", ) @@ -1257,7 +1256,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", ) @@ -1337,7 +1336,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", ) @@ -1367,7 +1366,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", ) @@ -1397,7 +1396,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", ) @@ -1427,7 +1426,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", ) @@ -1459,7 +1458,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", ) @@ -1488,7 +1487,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", ) @@ -1532,7 +1531,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", ) @@ -1624,7 +1623,7 @@ async def test_steer_control_error_non_blocking(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid=uuid4(), + agent_name="test-agent", stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1683,7 +1682,7 @@ async def test_deny_control_error_blocks(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid=uuid4(), + agent_name="test-agent", stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1732,7 +1731,7 @@ async def test_mixed_deny_and_steer_errors(self): engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid=uuid4(), + agent_name="test-agent", stage="pre", step=Step(type="llm", name="test-step", input="test", output=None), ) @@ -1827,7 +1826,7 @@ class MockControl: engine = ControlEngine(controls) request = EvaluationRequest( - agent_uuid=uuid4(), + agent_name="test-agent", stage="pre", step=Step( type="llm", 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 f7e3fb21..5735bd07 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, steer, 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): """ @@ -379,12 +400,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" ) @@ -392,6 +418,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): """ @@ -478,19 +509,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): """ @@ -499,15 +540,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 d08d4878..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", @@ -177,7 +176,7 @@ class InitAgentResponse(BaseModel): ) controls: list[Control] = Field( default_factory=list, - description="Active protection controls for the agent", + description="Active protection controls for the agent (if policy assigned)", ) overwrite_applied: bool = Field( default=False, @@ -202,15 +201,24 @@ class CreatePolicyResponse(BaseModel): policy_id: int = Field(description="Identifier of the created policy") -class GetAgentPoliciesResponse(BaseModel): - policy_ids: list[int] = Field( - default_factory=list, description="IDs of policies associated with the agent" +class SetPolicyResponse(BaseModel): + success: bool = Field(description="Whether the policy was successfully assigned") + old_policy_id: int | None = Field( + default=None, description="Previous policy id if one was replaced" ) +class GetPolicyResponse(BaseModel): + policy_id: int = Field(description="Identifier of the policy assigned to the agent") + + +class DeletePolicyResponse(BaseModel): + success: bool = Field(description="Whether the policy was successfully removed") + + class AgentControlsResponse(BaseModel): controls: list[Control] = Field( - description="List of active controls associated with the agent" + description="List of controls associated with the agent via its policy" ) @@ -240,18 +248,6 @@ class AssocResponse(BaseModel): success: bool = Field(description="Whether the association change succeeded") -class RemoveAgentControlResponse(BaseModel): - """Response for removing a direct agent-control association.""" - - success: bool = Field(description="Whether the request succeeded") - removed_direct_association: bool = Field( - description="True if a direct agent-control link was removed" - ) - control_still_active: bool = Field( - description="True if the control remains active via policy association(s)" - ) - - class GetControlDataResponse(BaseModel): data: ControlDefinition = Field(description="Control data payload") @@ -313,16 +309,13 @@ 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") - policy_ids: list[int] = Field( - default_factory=list, description="IDs of policies associated with 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") evaluator_count: int = Field(0, description="Number of evaluators registered with the agent") active_controls_count: int = Field( - 0, description="Number of active controls for this agent" + 0, description="Number of active controls from agent's policy" ) @@ -352,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): @@ -368,10 +360,6 @@ class ControlSummary(BaseModel): stages: list[str] | None = Field(None, description="Evaluation stages in scope") tags: list[str] = Field(default_factory=list, description="Control tags") used_by_agent: AgentRef | None = Field(None, description="Agent using this control") - # TODO: Follow-up with full `used_by_agents` list for richer attribution. - used_by_agents_count: int = Field( - 0, description="Number of unique agents using this control" - ) class ListControlsResponse(BaseModel): @@ -385,14 +373,10 @@ class DeleteControlResponse(BaseModel): """Response for deleting a control.""" success: bool = Field(..., description="Whether the control was deleted") - dissociated_from_policies: list[int] = Field( + dissociated_from: list[int] = Field( default_factory=list, description="Policy IDs the control was removed from before deletion", ) - dissociated_from_agents: list[str] = Field( - default_factory=list, - description="Agent IDs the control was removed from before deletion", - ) class PatchControlRequest(BaseModel): diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index ae12b752..621c9a08 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", ) @@ -52,7 +51,6 @@ async def handle_input(user_message: str) -> str: from collections.abc import Callable, Coroutine from datetime import UTC, datetime from typing import Any, Literal, TypeVar -from uuid import UUID import httpx from agent_control_models import ( @@ -107,11 +105,12 @@ 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__) + # ============================================================================ # Global State # ============================================================================ @@ -255,7 +254,7 @@ async def refresh_controls_async() -> list[dict[str, Any]] | None: try: async with AgentControlClient(base_url=_server_url, api_key=_api_key) as client: - response = await agents.list_agent_controls(client, _current_agent.agent_id) + response = await agents.list_agent_controls(client, _current_agent.agent_name) refreshed_controls = _publish_server_controls(response.get("controls", [])) logger.info("Refreshed %d control(s) from server", len(refreshed_controls or [])) return refreshed_controls @@ -314,7 +313,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, @@ -339,8 +337,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 @@ -365,8 +362,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=[ @@ -391,16 +387,15 @@ async def handle(message: str): """ global _current_agent, _control_engine, _client, _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) if policy_refresh_interval_seconds < 0: raise ValueError("policy_refresh_interval_seconds must be >= 0") @@ -414,8 +409,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, @@ -476,16 +470,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) @@ -518,7 +512,7 @@ def run_in_thread() -> None: server_controls = _run_coro_in_new_loop(register()) 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) @@ -552,21 +546,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: @@ -579,7 +573,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'])}") @@ -589,13 +583,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: @@ -608,7 +602,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" @@ -628,13 +621,13 @@ 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, - policy_ids, created_at, step_count, evaluator_count + - 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 Raises: @@ -648,7 +641,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( @@ -663,93 +656,6 @@ async def main(): return await agents.list_agents(client, cursor=cursor, limit=limit) -# ============================================================================ -# Agent Association Convenience Functions -# ============================================================================ - - -async def get_agent_policies( - agent_id: str | UUID, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - List policy IDs associated with an agent. - """ - _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_policies(client, agent_id) - - -async def add_policy_to_agent( - agent_id: str | UUID, - policy_id: int, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - Associate a policy with an agent. - """ - _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.add_agent_policy(client, agent_id, policy_id) - - -async def remove_policy_from_agent( - agent_id: str | UUID, - policy_id: int, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - Remove a specific policy association from an agent. - """ - _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.remove_agent_policy_association(client, agent_id, policy_id) - - -async def clear_agent_policies( - agent_id: str | UUID, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - Remove all policy associations from an agent. - """ - _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.remove_agent_policies(client, agent_id) - - -async def add_control_to_agent( - agent_id: str | UUID, - control_id: int, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - Associate a control directly with an agent. - """ - _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.add_agent_control(client, agent_id, control_id) - - -async def remove_control_from_agent( - agent_id: str | UUID, - control_id: int, - server_url: str | None = None, - api_key: str | None = None, -) -> dict[str, Any]: - """ - Remove a direct control association from an agent. - """ - _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.remove_agent_control(client, agent_id, control_id) - - # ============================================================================ # Control Management Convenience Functions # ============================================================================ @@ -930,7 +836,7 @@ async def delete_control( """ Delete a control from the server. - By default, deletion fails if the control is associated with any policy or agent. + By default, deletion fails if the control is associated with any policy. Use force=True to automatically dissociate and delete. Args: @@ -942,8 +848,7 @@ async def delete_control( Returns: Dictionary containing: - success: True if control was deleted - - dissociated_from_policies: List of policy IDs the control was removed from - - dissociated_from_agents: List of agent UUIDs the control was removed from + - dissociated_from: List of policy IDs the control was removed from Raises: httpx.HTTPError: If request fails @@ -957,11 +862,7 @@ async def delete_control( async def main(): # Force delete result = await agent_control.delete_control(5, force=True) - print( - "Deleted, removed from " - f"{len(result['dissociated_from_policies'])} policies and " - f"{len(result['dissociated_from_agents'])} agents" - ) + print(f"Deleted, removed from {len(result['dissociated_from'])} policies") asyncio.run(main()) """ @@ -1165,12 +1066,6 @@ async def main(): # Agent management "get_agent", "list_agents", - "get_agent_policies", - "add_policy_to_agent", - "remove_policy_from_agent", - "clear_agent_policies", - "add_control_to_agent", - "remove_control_from_agent", # Control management "create_control", "list_controls", diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index 27b0a122..b14e69a8 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -1,14 +1,13 @@ """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 agent_control_models import Agent from agent_control_models.server import AgentControlsResponse from .client import AgentControlClient -from .validation import ensure_uuid_str +from .validation import ensure_agent_name async def register_agent( @@ -17,33 +16,11 @@ 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() agent_dict = agent.to_dict() - 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 or [], @@ -55,33 +32,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()) @@ -91,186 +45,54 @@ 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_ids, 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()) -async def get_agent_policies( - client: AgentControlClient, - agent_id: str | UUID, -) -> dict[str, Any]: - """ - Get policy IDs associated with an agent. - - Args: - client: AgentControlClient instance - agent_id: UUID string or UUID instance - - Returns: - Dictionary containing: - - policy_ids: IDs of policies associated with the agent - - Raises: - httpx.HTTPError: If request fails - - Example: - async with AgentControlClient() as client: - policies = await get_agent_policies(client, agent_id) - print(f"Policy IDs: {policies['policy_ids']}") - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/policies") - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - -async def add_agent_policy( +async def get_agent_policy( client: AgentControlClient, - agent_id: str | UUID, - policy_id: int, + agent_name: str, ) -> dict[str, Any]: - """ - Associate a policy with an agent. - - This operation is idempotent. - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.post( - f"/api/v1/agents/{agent_id_str}/policies/{policy_id}" - ) + """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_association( +async def remove_agent_policy( client: AgentControlClient, - agent_id: str | UUID, - policy_id: int, + agent_name: str, ) -> dict[str, Any]: - """ - Remove a specific policy association from an agent. - - This operation is idempotent for existing agent/policy resources: - removing a non-associated link is a no-op. Missing agent/policy - resources still return 404. - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.delete( - f"/api/v1/agents/{agent_id_str}/policies/{policy_id}" - ) - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - -async def remove_agent_policies( - client: AgentControlClient, - agent_id: str | UUID, -) -> dict[str, Any]: - """ - Remove all policy associations 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 - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.delete(f"/api/v1/agents/{agent_id_str}/policies") - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - -async def add_agent_control( - client: AgentControlClient, - agent_id: str | UUID, - control_id: int, -) -> dict[str, Any]: - """ - Associate a control directly with an agent. - - This operation is idempotent. - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.post( - f"/api/v1/agents/{agent_id_str}/controls/{control_id}" - ) - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - -async def remove_agent_control( - client: AgentControlClient, - agent_id: str | UUID, - control_id: int, -) -> dict[str, Any]: - """ - Remove a direct control association from an agent. - - This operation is idempotent. - """ - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.delete( - f"/api/v1/agents/{agent_id_str}/controls/{control_id}" - ) + """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()) async def list_agent_controls( client: AgentControlClient, - agent_id: str | UUID, + agent_name: str, ) -> dict[str, Any]: """List active controls associated with an agent.""" - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/controls") + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.get(f"/api/v1/agents/{normalized_name}/controls") response.raise_for_status() return cast(dict[str, Any], response.json()) async def list_agent_controls_typed( client: AgentControlClient, - agent_id: str | UUID, + agent_name: str, ) -> AgentControlsResponse: """List active controls associated with an agent (typed response).""" - agent_id_str = ensure_uuid_str(agent_id) - response = await client.http_client.get(f"/api/v1/agents/{agent_id_str}/controls") + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.get(f"/api/v1/agents/{normalized_name}/controls") response.raise_for_status() return AgentControlsResponse.model_validate(response.json()) diff --git a/sdks/python/src/agent_control/control_decorators.py b/sdks/python/src/agent_control/control_decorators.py index 57381f04..24789513 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 @@ -35,7 +34,6 @@ async def chat(message: str) -> str: from collections.abc import Callable from dataclasses import dataclass, field from typing import Any, TypeVar -from uuid import UUID from agent_control_models import Step @@ -64,7 +62,6 @@ class ControlContext: control wrappers, including stats tracking, result processing, and logging. """ - agent_uuid: str agent_name: str server_url: str func: Callable @@ -228,14 +225,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. @@ -261,13 +258,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 @@ -355,7 +352,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 }, @@ -671,7 +668,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, @@ -688,10 +684,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, ControlSteerError): @@ -712,10 +708,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, ControlSteerError): @@ -760,8 +756,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) @@ -789,7 +784,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}/policies/{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/controls.py b/sdks/python/src/agent_control/controls.py index adbf5d21..7050cc5a 100644 --- a/sdks/python/src/agent_control/controls.py +++ b/sdks/python/src/agent_control/controls.py @@ -335,7 +335,7 @@ async def delete_control( """ Delete a control by ID. - By default, deletion fails if the control is associated with any policy or agent. + By default, deletion fails if the control is associated with any policy. Use force=True to automatically dissociate and delete. Args: @@ -346,8 +346,7 @@ async def delete_control( Returns: Dictionary containing: - success: True if control was deleted - - dissociated_from_policies: List of policy IDs the control was removed from - - dissociated_from_agents: List of agent UUIDs the control was removed from + - dissociated_from: List of policy IDs the control was removed from Raises: httpx.HTTPError: If request fails @@ -363,11 +362,7 @@ async def delete_control( if e.response.status_code == 409: # Force delete result = await delete_control(client, control_id=5, force=True) - print( - "Removed from " - f"{len(result['dissociated_from_policies'])} policies and " - f"{len(result['dissociated_from_agents'])} agents" - ) + print(f"Removed from {len(result['dissociated_from'])} policies") """ params = {"force": force} response = await client.http_client.delete( diff --git a/sdks/python/src/agent_control/evaluation.py b/sdks/python/src/agent_control/evaluation.py index 235b8000..a42fa835 100644 --- a/sdks/python/src/agent_control/evaluation.py +++ b/sdks/python/src/agent_control/evaluation.py @@ -3,57 +3,33 @@ from dataclasses import dataclass from datetime import UTC, datetime from typing import Any, Literal, cast -from uuid import UUID + +from agent_control_engine import list_evaluators +from agent_control_engine.core import ControlEngine +from agent_control_models import ( + ControlDefinition, + ControlExecutionEvent, + ControlMatch, + EvaluationRequest, + EvaluationResponse, + EvaluationResult, + EvaluatorResult, + Step, +) 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 -# Import models if available -try: - from agent_control_engine import list_evaluators - from agent_control_engine.core import ControlEngine - from agent_control_models import ( - ControlDefinition, - ControlExecutionEvent, - ControlMatch, - EvaluationRequest, - EvaluationResponse, - EvaluationResult, - EvaluatorResult, - Step, - ) - - MODELS_AVAILABLE = True - ENGINE_AVAILABLE = True -except ImportError: - MODELS_AVAILABLE = False - ENGINE_AVAILABLE = False - # Runtime fallbacks - Step = Any # type: ignore - EvaluationRequest = Any # type: ignore - EvaluationResponse = Any # type: ignore - EvaluationResult = Any # type: ignore - EvaluatorResult = Any # type: ignore - ControlDefinition = Any # type: ignore - ControlMatch = Any # type: ignore - ControlEngine = Any # type: ignore - ControlExecutionEvent = Any # type: ignore - - 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" @@ -77,8 +53,6 @@ def _emit_local_events( """ if not is_observability_enabled(): return - if not ENGINE_AVAILABLE: - return global _trace_warning_logged # noqa: PLW0603 if not trace_id or not span_id: @@ -95,31 +69,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,93 +104,24 @@ 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, - 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: - step_dict = { - "type": getattr(step, "type", None), - "name": getattr(step, "name", None), - "input": getattr(step, "input", None), - "output": getattr(step, "output", None), - "context": getattr(step, "context", None), - } - step_dict = {k: v for k, v in step_dict.items() if v is not None} - - if not step_dict.get("name"): - raise ValueError("step.name is required for evaluation requests") - - request_payload = { - "agent_uuid": str(agent_uuid), - "step": step_dict, - "stage": stage, - } + request = EvaluationRequest( + agent_name=normalized_name, + step=step, + stage=stage, + ) + request_payload = request.model_dump(mode="json") response = await client.http_client.post("/api/v1/evaluation", json=request_payload) response.raise_for_status() - 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)) + return cast(EvaluationResult, EvaluationResult.from_dict(response.json())) @dataclass @@ -232,35 +137,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,13 +173,13 @@ 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. @@ -303,7 +195,7 @@ async def check_evaluation_with_local( Args: client: AgentControlClient instance - agent_uuid: UUID of the agent making the request + agent_name: Normalized agent identifier step: Step payload to evaluate stage: 'pre' for pre-execution check, 'post' for post-execution check controls: List of control dicts from initAgent response @@ -314,80 +206,56 @@ async def check_evaluation_with_local( 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, - ) """ - if not ENGINE_AVAILABLE: - raise RuntimeError( - "Local evaluation requires agent_control_engine. " - "Install with: pip install agent-control-engine" - ) - + normalized_name = ensure_agent_name(agent_name) # Partition controls by local flag 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,14 +265,13 @@ 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}", ), steering_context=None, ) ) 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 @@ -417,27 +284,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( @@ -450,22 +316,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)) @@ -480,7 +346,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( @@ -493,5 +358,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 b3266744..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,17 +151,17 @@ 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]: """ - Associate a policy with an agent. + Assign a policy to an agent. - This operation is idempotent and additive: agents can be associated with many policies. + This makes the policy active for the agent. Any existing policy assignment is replaced. 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}/policies/{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 eb9ad525..20277e22 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -1,10 +1,8 @@ -"""SDK agent_id validation behavior tests.""" +"""SDK agent name validation behavior tests.""" -from unittest.mock import AsyncMock, MagicMock, patch -from uuid import uuid4 - -import agent_control +from unittest.mock import AsyncMock, MagicMock import pytest + from agent_control import agents, policies @@ -17,90 +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_policies_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_policies(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_policies_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_policies(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_remove_agent_policy_association_rejects_invalid_uuid() -> None: +async def test_list_agents_normalizes_cursor() -> None: client = MagicMock() client.http_client = MagicMock() - client.http_client.delete = AsyncMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) - with pytest.raises(ValueError, match="agent_id must be a valid UUID"): - await agents.remove_agent_policy_association( - client, "not-a-uuid", policy_id=1 - ) + await agents.list_agents(client, cursor="Agent-Example_01", limit=5) - client.http_client.delete.assert_not_called() + 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_uuid() -> None: +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_id = uuid4() - await agents.get_agent(client, agent_id) + agent_name = "Agent-Example_01" + await agents.get_agent(client, agent_name) - client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_id}") + client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01") @pytest.mark.asyncio -async def test_clear_agent_policies_calls_agents_module() -> None: - agent_id = uuid4() +async def test_get_agent_policy_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) + + await agents.get_agent_policy(client, "Agent-Example_01") + + client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy") + - with patch( - "agent_control.__init__.agents.remove_agent_policies", new_callable=AsyncMock - ) as mock_remove: - mock_remove.return_value = {"success": True} +@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()) - result = await agent_control.clear_agent_policies(agent_id) + await agents.remove_agent_policy(client, "Agent-Example_01") - assert result == {"success": True} - assert mock_remove.await_count == 1 - assert mock_remove.await_args.args[1] == 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 a59ab716..216fe032 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 @@ -228,14 +228,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 @@ -258,14 +258,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 @@ -291,14 +291,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) @@ -330,14 +330,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) @@ -360,14 +360,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) @@ -390,14 +390,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) @@ -597,14 +597,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 @@ -635,14 +635,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 @@ -673,14 +673,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 2366b299..2f47c1a2 100644 --- a/sdks/python/tests/test_evaluation.py +++ b/sdks/python/tests/test_evaluation.py @@ -20,7 +20,7 @@ async def test_check_evaluation_requires_step_name_before_server_call(): with pytest.raises(ValidationError): 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", ) @@ -44,7 +44,7 @@ def json(self) -> dict[str, object]: result = await evaluation.check_evaluation( client=client, - agent_uuid=UUID("00000000-0000-0000-0000-000000000001"), + agent_name="Agent-Example_01", step={"type": "llm", "name": "chat", "input": "hello"}, stage="pre", ) @@ -55,7 +55,7 @@ def json(self) -> dict[str, object]: client.http_client.post.assert_awaited_once_with( "/api/v1/evaluation", json={ - "agent_uuid": "00000000-0000-0000-0000-000000000001", + "agent_name": "agent-example_01", "step": { "type": "llm", "name": "chat", diff --git a/sdks/python/tests/test_init_conflict.py b/sdks/python/tests/test_init_conflict.py index 47d9d2ab..3050faa5 100644 --- a/sdks/python/tests/test_init_conflict.py +++ b/sdks/python/tests/test_init_conflict.py @@ -18,7 +18,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( @@ -30,8 +30,7 @@ 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", policy_refresh_interval_seconds=0, ) diff --git a/sdks/python/tests/test_init_step_merge.py b/sdks/python/tests/test_init_step_merge.py index 0c5f098b..669e1236 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, policy_refresh_interval_seconds=0, ) @@ -103,8 +102,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]}", policy_refresh_interval_seconds=0, ) @@ -147,8 +145,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]}", policy_refresh_interval_seconds=0, ) @@ -185,11 +182,7 @@ def test_init_logs_agent_updated_when_registration_already_exists( new=register_agent_mock, ): with caplog.at_level(logging.INFO): - agent_control.init( - agent_name=agent_name, - agent_id=str(uuid4()), - policy_refresh_interval_seconds=0, - ) + agent_control.init(agent_name=agent_name, policy_refresh_interval_seconds=0) # Then the SDK emits the "updated" log branch. assert "Agent updated" in caplog.text @@ -214,8 +207,7 @@ async def test_refresh_controls_calls_agent_controls_endpoint() -> None: new=list_agent_controls_mock, ): agent_control.init( - agent_name="Refresh Strict Agent", - agent_id=str(uuid4()), + agent_name=f"agent-{uuid4().hex[:12]}", policy_refresh_interval_seconds=0, ) diff --git a/sdks/python/tests/test_init_validation.py b/sdks/python/tests/test_init_validation.py index 44e633a4..19662aa3 100644 --- a/sdks/python/tests/test_init_validation.py +++ b/sdks/python/tests/test_init_validation.py @@ -7,16 +7,15 @@ from agent_control_models import EvaluatorResult as ModelEvaluatorResult -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_rejects_negative_policy_refresh_interval() -> None: with pytest.raises(ValueError, match="policy_refresh_interval_seconds must be >= 0"): agent_control.init( agent_name="negative-interval-agent", - agent_id="550e8400-e29b-41d4-a716-446655440000", policy_refresh_interval_seconds=-1, ) diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index 6a2de95b..144081d5 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -33,11 +33,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(), @@ -81,10 +78,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 @@ -92,7 +89,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 @@ -147,7 +144,7 @@ async def test_list_agent_controls_typed_returns_model( # WHEN: controls are requested via the typed API wrapper. response = await agent_control.agents.list_agent_controls_typed( client, - test_agent["agent_id"], + test_agent["agent_name"], ) # THEN: a typed model is returned. @@ -166,7 +163,7 @@ async def test_list_agent_controls_returns_dict_payload( # WHEN: controls are requested via the dict API wrapper. response = await agent_control.agents.list_agent_controls( client, - test_agent["agent_id"], + test_agent["agent_name"], ) # THEN: a dict payload is returned with controls list. @@ -188,14 +185,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") @@ -217,8 +214,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, @@ -230,8 +226,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 78c0d657..94661ab0 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, @@ -748,7 +747,7 @@ async def test_malformed_server_control_still_calls_server(self, agent_uuid, llm assert result.is_safe is True @pytest.mark.asyncio - async def test_local_evaluation_includes_steering_context(self, agent_uuid, llm_payload): + async def test_local_evaluation_includes_steering_context(self, agent_name, llm_payload): """Test that local evaluation includes steering_context in response. Given: A local steer control with steering_context configured @@ -785,7 +784,7 @@ async def test_local_evaluation_includes_steering_context(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_policy_refresh_loop.py b/sdks/python/tests/test_policy_refresh_loop.py index d3d0b317..cdab95f3 100644 --- a/sdks/python/tests/test_policy_refresh_loop.py +++ b/sdks/python/tests/test_policy_refresh_loop.py @@ -10,8 +10,6 @@ import agent_control import pytest -TEST_AGENT_ID = "550e8400-e29b-41d4-a716-446655440000" - @pytest.fixture(autouse=True) def _reset_policy_refresh_state() -> Generator[None, None, None]: @@ -46,7 +44,6 @@ def test_init_starts_policy_refresh_loop_by_default() -> None: ) as start_loop_mock: agent_control.init( agent_name="default-refresh-agent", - agent_id=TEST_AGENT_ID, ) # Then: the loop starts with the default interval (60s). @@ -70,7 +67,6 @@ def test_init_disables_policy_refresh_loop_when_interval_is_zero() -> None: ) as start_loop_mock: agent_control.init( agent_name="disabled-refresh-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) @@ -97,12 +93,10 @@ def test_reinit_stops_and_restarts_policy_refresh_loop() -> None: ) as start_loop_mock: agent_control.init( agent_name="reinit-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=60, ) agent_control.init( agent_name="reinit-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=5, ) @@ -265,7 +259,6 @@ def test_refresh_controls_sync_without_running_loop_uses_refresh_endpoint() -> N ): agent_control.init( agent_name="sync-refresh-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) @@ -297,7 +290,6 @@ async def test_refresh_controls_sync_with_running_loop_uses_worker_thread() -> N ): agent_control.init( agent_name="async-refresh-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) @@ -329,7 +321,6 @@ async def test_refresh_fail_open_retains_previous_controls() -> None: ): agent_control.init( agent_name="fail-open-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) previous_snapshot = agent_control.get_server_controls() @@ -364,7 +355,6 @@ async def test_refresh_uses_swap_only_cache_publication() -> None: ): agent_control.init( agent_name="swap-only-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) old_snapshot = agent_control.get_server_controls() @@ -416,7 +406,6 @@ def reader() -> None: ): agent_control.init( agent_name="concurrent-refresh-agent", - agent_id=TEST_AGENT_ID, policy_refresh_interval_seconds=0, ) reader_thread = threading.Thread(target=reader, daemon=True) 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/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..90a39d03 --- /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 = '53d8427083e2' +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/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py b/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py deleted file mode 100644 index a106f2ae..00000000 --- a/server/alembic/versions/58e519e06e02_agent_policy_m2m_and_direct_agent_controls.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Revision ID: 58e519e06e02 -Revises: 53d8427083e2 -Create Date: 2026-02-23 13:39:21.295377 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "58e519e06e02" -down_revision = "53d8427083e2" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('agent_controls', - sa.Column('agent_uuid', sa.UUID(), nullable=False), - sa.Column('control_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['agent_uuid'], ['agents.agent_uuid'], ), - sa.ForeignKeyConstraint(['control_id'], ['controls.id'], ), - sa.PrimaryKeyConstraint('agent_uuid', 'control_id') - ) - op.create_index(op.f('ix_agent_controls_agent_uuid'), 'agent_controls', ['agent_uuid'], unique=False) - op.create_index(op.f('ix_agent_controls_control_id'), 'agent_controls', ['control_id'], unique=False) - op.create_table('agent_policies', - sa.Column('agent_uuid', sa.UUID(), nullable=False), - sa.Column('policy_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['agent_uuid'], ['agents.agent_uuid'], ), - sa.ForeignKeyConstraint(['policy_id'], ['policies.id'], ), - sa.PrimaryKeyConstraint('agent_uuid', 'policy_id') - ) - op.create_index(op.f('ix_agent_policies_agent_uuid'), 'agent_policies', ['agent_uuid'], unique=False) - op.create_index(op.f('ix_agent_policies_policy_id'), 'agent_policies', ['policy_id'], unique=False) - op.execute( - sa.text( - """ - INSERT INTO agent_policies (agent_uuid, policy_id) - SELECT agent_uuid, policy_id - FROM agents - WHERE policy_id IS NOT NULL - ON CONFLICT (agent_uuid, policy_id) DO NOTHING - """ - ) - ) - op.drop_index(op.f('ix_agents_policy_id'), table_name='agents') - op.drop_constraint(op.f('agents_policy_id_fkey'), 'agents', type_='foreignkey') - op.drop_column('agents', 'policy_id') - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - # NOTE: Downgrade can only restore one policy per agent via agents.policy_id. - # If multiple policies were associated to an agent, all but the minimum policy_id are lost. - op.add_column('agents', sa.Column('policy_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.execute( - sa.text( - """ - UPDATE agents AS a - SET policy_id = ap.policy_id - FROM ( - SELECT agent_uuid, MIN(policy_id) AS policy_id - FROM agent_policies - GROUP BY agent_uuid - ) AS ap - WHERE a.agent_uuid = ap.agent_uuid - """ - ) - ) - op.create_foreign_key(op.f('agents_policy_id_fkey'), 'agents', 'policies', ['policy_id'], ['id']) - op.create_index(op.f('ix_agents_policy_id'), 'agents', ['policy_id'], unique=False) - op.drop_index(op.f('ix_agent_policies_policy_id'), table_name='agent_policies') - op.drop_index(op.f('ix_agent_policies_agent_uuid'), table_name='agent_policies') - op.drop_table('agent_policies') - # NOTE: Direct agent-control associations have no representation in the pre-upgrade schema. - # Dropping this table intentionally discards those associations during downgrade. - op.drop_index(op.f('ix_agent_controls_control_id'), table_name='agent_controls') - op.drop_index(op.f('ix_agent_controls_agent_uuid'), table_name='agent_controls') - op.drop_table('agent_controls') - # ### end Alembic commands ### diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index e479aedd..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 @@ -8,11 +7,11 @@ from agent_control_models.server import ( AgentControlsResponse, AgentSummary, - AssocResponse, ConflictMode, + DeletePolicyResponse, EvaluatorSchema, - GetAgentPoliciesResponse, GetAgentResponse, + GetPolicyResponse, InitAgentEvaluatorRemoval, InitAgentOverwriteChanges, InitAgentRequest, @@ -21,14 +20,13 @@ PaginationInfo, PatchAgentRequest, PatchAgentResponse, - RemoveAgentControlResponse, + SetPolicyResponse, StepKey, ) from fastapi import APIRouter, Depends from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import BaseModel, ValidationError -from sqlalchemy import delete, func, or_, select, union_all -from sqlalchemy.dialects.postgresql import insert as pg_insert +from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -45,16 +43,14 @@ AgentData, Control, Policy, - agent_controls, - agent_policies, 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, validate_config_against_schema, ) -from ..services.query_utils import escape_like_pattern from ..services.schema_compat import ( check_schema_compatibility, format_compatibility_error, @@ -89,8 +85,18 @@ def _get_builtin_evaluator_names() -> set[str]: return _BUILTIN_EVALUATOR_NAMES -def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[str]: - """Validate controls can run on this agent.""" +async def _validate_policy_controls_for_agent( + agent: Agent, policy_id: int, db: AsyncSession +) -> list[str]: + """Validate all controls in a policy can run on this agent. + + Checks that agent-scoped evaluators referenced by controls: + 1. Exist on the agent (registered via initAgent) + 2. Have config that validates against the evaluator's schema + + Returns: + List of error messages (empty if all valid) + """ errors: list[str] = [] # Parse agent's registered evaluators @@ -101,6 +107,9 @@ def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[ agent_evaluators = {e.name: e for e in (agent_data.evaluators or [])} + # Get all controls for this policy + controls = await list_controls_for_policy(policy_id, db) + for control in controls: if not control.data: continue @@ -146,14 +155,6 @@ def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[ return errors -async def _validate_policy_controls_for_agent( - agent: Agent, policy_id: int, db: AsyncSession -) -> list[str]: - """Validate all controls in a policy can run on this agent.""" - controls = await list_controls_for_policy(policy_id, db) - return _validate_controls_for_agent(agent, controls) - - def _step_registration_changed(existing_step: StepSchema, incoming_step: StepSchema) -> bool: """Return True when non-key step registration fields differ.""" return ( @@ -176,12 +177,12 @@ async def _build_overwrite_evaluator_removals( db: AsyncSession, ) -> list[InitAgentEvaluatorRemoval]: """Build evaluator removal details, including active-control references.""" - if not removed_evaluators: + if not removed_evaluators or agent.policy_id is None: return [InitAgentEvaluatorRemoval(name=name) for name in sorted(removed_evaluators)] try: controls = await list_controls_for_agent( - agent.agent_uuid, + agent.name, db, allow_invalid_step_name_regex=True, ) @@ -238,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 associations, + 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) @@ -254,9 +255,7 @@ async def list_agents( limit = min(max(1, limit), _MAX_PAGINATION_LIMIT) # Build base filter for name search - name_filter = ( - Agent.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") if name else None - ) + name_filter = Agent.name.ilike(f"%{name}%") if name else None # Get total count (with name filter if provided) count_query = select(func.count()).select_from(Agent) @@ -266,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: @@ -275,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) @@ -305,59 +298,37 @@ 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) - - # Batch query: Get policy IDs and active control counts for all agents at once - control_counts_map: dict[UUID, int] = {} - policy_ids_map: dict[UUID, list[int]] = {} + 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[str, int] = {} if agents: - agent_uuids = [agent.agent_uuid for agent in agents] - - # Policy associations per agent - policy_ids_query = ( - select(agent_policies.c.agent_uuid, agent_policies.c.policy_id) - .where(agent_policies.c.agent_uuid.in_(agent_uuids)) - .order_by(agent_policies.c.agent_uuid, agent_policies.c.policy_id) - ) - policy_ids_result = await db.execute(policy_ids_query) - for agent_uuid, policy_id in policy_ids_result.all(): - policy_ids_map.setdefault(agent_uuid, []).append(policy_id) - - # Active controls per agent (policy-derived + direct controls, de-duplicated) - policy_associations = ( - select( - agent_policies.c.agent_uuid.label("agent_uuid"), - policy_controls.c.control_id.label("control_id"), - ) - .select_from( - agent_policies.join( - policy_controls, agent_policies.c.policy_id == policy_controls.c.policy_id - ) - ) - .where(agent_policies.c.agent_uuid.in_(agent_uuids)) - ) - direct_associations = select( - agent_controls.c.agent_uuid.label("agent_uuid"), - agent_controls.c.control_id.label("control_id"), - ).where(agent_controls.c.agent_uuid.in_(agent_uuids)) - all_associations = union_all(policy_associations, direct_associations).subquery() - control_counts_query = ( select( - all_associations.c.agent_uuid, - func.count(func.distinct(all_associations.c.control_id)).label("count"), + Agent.name, + func.count(func.distinct(policy_controls.c.control_id)).label("count"), ) - .join(Control, all_associations.c.control_id == Control.id) + .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.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) or_( Control.data["enabled"].astext == "true", ~Control.data.has_key("enabled"), ), ) - .group_by(all_associations.c.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()} @@ -378,13 +349,12 @@ 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_ids=policy_ids_map.get(agent.agent_uuid, []), + policy_id=agent.policy_id, created_at=agent.created_at.isoformat() if agent.created_at else None, step_count=step_count, evaluator_count=evaluator_count, @@ -417,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 @@ -430,11 +398,7 @@ async def init_agent( db: Database session (injected) Returns: - InitAgentResponse with created flag and active controls - - Raises: - HTTPException 409: Agent name exists with different UUID - HTTPException 500: Database error during creation/update + InitAgentResponse with created flag and active controls (if policy assigned) """ # Check for evaluator name collisions with built-in evaluators builtin_names = _get_builtin_evaluator_names() @@ -480,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 @@ -530,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) @@ -543,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( @@ -553,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) @@ -811,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( @@ -820,8 +729,10 @@ async def init_agent( operation="update", ) - # Include all active controls (policy-derived and direct). - controls = await list_controls_for_agent(existing.agent_uuid, db) + # 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.name, db) return InitAgentResponse( created=created, @@ -832,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: @@ -854,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( @@ -888,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( @@ -903,33 +818,46 @@ async def get_agent(agent_id: UUID, db: AsyncSession = Depends(get_async_db)) -> ) -async def _get_agent_or_404(agent_id: UUID, db: AsyncSession) -> Agent: - """Get an agent or raise AGENT_NOT_FOUND.""" - result = await db.execute(select(Agent).where(Agent.agent_uuid == agent_id)) +@router.post( + "/{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_name: str, policy_id: int, db: AsyncSession = Depends(get_async_db) +) -> SetPolicyResponse: + """ + Assign a policy to an agent, replacing any existing policy assignment. + + The agent will immediately inherit all controls from the assigned policy. + + Args: + agent_name: Agent identifier + 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 + """ + agent_name = normalize_agent_name_or_422(agent_name) + # Find agent + 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.", ) - return agent - - -@router.post( - "/{agent_id}/policies/{policy_id}", - response_model=AssocResponse, - summary="Associate policy with agent", - response_description="Success confirmation", -) -async def add_agent_policy( - agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) -) -> AssocResponse: - """Associate a policy with an agent (idempotent).""" - agent = await _get_agent_or_404(agent_id, db) + # Find policy by id policy_result = await db.execute(select(Policy).where(Policy.id == policy_id)) policy: Policy | None = policy_result.scalars().first() if policy is None: @@ -941,6 +869,7 @@ async def add_agent_policy( hint="Verify the policy ID is correct and the policy has been created.", ) + # Validate controls can run on this agent validation_errors = await _validate_policy_controls_for_agent(agent, policy_id, db) if validation_errors: raise BadRequestError( @@ -958,299 +887,199 @@ async def add_agent_policy( ], ) + # Store old policy ID if exists + old_policy_id: int | None = None + if agent.policy_id is not None: + old_policy_id = agent.policy_id + + # Assign new policy + agent.policy_id = policy.id try: - stmt = ( - pg_insert(agent_policies) - .values(agent_uuid=agent_id, policy_id=policy_id) - .on_conflict_do_nothing() - ) - await db.execute(stmt) await db.commit() except Exception: await db.rollback() _logger.error( - "Failed to associate policy '%s' with agent '%s' (%s)", - policy_id, - agent.name, - agent_id, + f"Failed to assign policy '{policy_id}' to agent '{agent.name}' ({agent_name})", exc_info=True, ) raise DatabaseError( - detail=f"Failed to associate policy with agent '{agent.name}': database error", + detail=f"Failed to assign policy to agent '{agent.name}': database error", resource="Agent", - operation="add policy association", + operation="assign policy", ) - return AssocResponse(success=True) + return SetPolicyResponse(success=True, old_policy_id=old_policy_id) @router.get( - "/{agent_id}/policies", - response_model=GetAgentPoliciesResponse, - summary="List policies associated with agent", - response_description="List of policy IDs", + "/{agent_name}/policy", + response_model=GetPolicyResponse, + summary="Get agent's assigned policy", + response_description="Policy ID", ) -async def get_agent_policies( - agent_id: UUID, db: AsyncSession = Depends(get_async_db) -) -> GetAgentPoliciesResponse: - """List policy IDs associated with an agent.""" - await _get_agent_or_404(agent_id, db) - result = await db.execute( - select(agent_policies.c.policy_id) - .where(agent_policies.c.agent_uuid == agent_id) - .order_by(agent_policies.c.policy_id) - ) - return GetAgentPoliciesResponse(policy_ids=[row[0] for row in result.all()]) +async def get_agent_policy( + agent_name: str, db: AsyncSession = Depends(get_async_db) +) -> GetPolicyResponse: + """ + Retrieve the policy currently assigned to an agent. + Args: + agent_name: Agent identifier + db: Database session (injected) -@router.delete( - "/{agent_id}/policies/{policy_id}", - response_model=AssocResponse, - summary="Remove policy association from agent", - response_description="Success confirmation", -) -async def remove_agent_policy( - agent_id: UUID, policy_id: int, db: AsyncSession = Depends(get_async_db) -) -> AssocResponse: - """Remove a policy association from an agent. + Returns: + GetPolicyResponse with policy ID - Idempotent for existing resources: removing a non-associated link is a no-op. - Missing agent/policy resources still return 404. + Raises: + HTTPException 404: Agent not found or agent has no policy assigned """ - await _get_agent_or_404(agent_id, db) + agent_name = normalize_agent_name_or_422(agent_name) + # Find agent + 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 name '{agent_name}' not found", + resource="Agent", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", + ) - policy_result = await db.execute(select(Policy.id).where(Policy.id == policy_id)) - if policy_result.first() is None: + # Check if agent has a policy + if agent.policy_id is None: raise NotFoundError( error_code=ErrorCode.POLICY_NOT_FOUND, - detail=f"Policy with ID '{policy_id}' not found", + detail=f"Agent '{agent.name}' has no policy assigned", resource="Policy", - resource_id=str(policy_id), - hint="Verify the policy ID is correct and the policy has been created.", + hint="Assign a policy to the agent using POST /{agent_name}/policy/{policy_id}.", ) - try: - await db.execute( - delete(agent_policies).where( - (agent_policies.c.agent_uuid == agent_id) - & (agent_policies.c.policy_id == policy_id) - ) - ) - await db.commit() - except Exception: - await db.rollback() - _logger.error( - "Failed to remove policy '%s' from agent '%s'", - policy_id, - agent_id, - exc_info=True, - ) - raise DatabaseError( - detail=f"Failed to remove policy association from agent '{agent_id}': database error", - resource="Agent", - operation="remove policy association", + # Find policy + policy_result = await db.execute(select(Policy).where(Policy.id == agent.policy_id)) + policy: Policy | None = policy_result.scalars().first() + if policy is None: + raise NotFoundError( + error_code=ErrorCode.POLICY_NOT_FOUND, + detail=( + f"Policy with ID '{agent.policy_id}' not found " + f"(referenced by agent '{agent.name}')" + ), + resource="Policy", + resource_id=str(agent.policy_id), + hint="The referenced policy may have been deleted. Assign a new policy to the agent.", ) - return AssocResponse(success=True) + return GetPolicyResponse(policy_id=policy.id) @router.delete( - "/{agent_id}/policies", - response_model=AssocResponse, - summary="Remove all policy associations from agent", + "/{agent_name}/policy", + response_model=DeletePolicyResponse, + summary="Remove agent's policy assignment", response_description="Success confirmation", ) -async def remove_all_agent_policies( - agent_id: UUID, db: AsyncSession = Depends(get_async_db) -) -> AssocResponse: - """Remove all policy associations from an agent.""" - await _get_agent_or_404(agent_id, db) +async def delete_agent_policy( + agent_name: str, db: AsyncSession = Depends(get_async_db) +) -> DeletePolicyResponse: + """ + Remove the policy assignment from an agent. - try: - await db.execute(delete(agent_policies).where(agent_policies.c.agent_uuid == agent_id)) - await db.commit() - except Exception: - await db.rollback() - _logger.error( - "Failed to remove all policies from agent '%s'", - agent_id, - exc_info=True, - ) - raise DatabaseError( - detail=f"Failed to remove policy associations from agent '{agent_id}': database error", - resource="Agent", - operation="remove all policy associations", - ) + The agent will no longer have any protection controls active. - return AssocResponse(success=True) + Args: + agent_name: Agent identifier + db: Database session (injected) + Returns: + DeletePolicyResponse with success flag -@router.post( - "/{agent_id}/controls/{control_id}", - response_model=AssocResponse, - summary="Associate control directly with agent", - response_description="Success confirmation", -) -async def add_agent_control( - agent_id: UUID, control_id: int, db: AsyncSession = Depends(get_async_db) -) -> AssocResponse: - """Associate a control directly with an agent (idempotent).""" - agent = await _get_agent_or_404(agent_id, db) - - control_result = await db.execute(select(Control).where(Control.id == control_id)) - control: Control | None = control_result.scalars().first() - if control is None: + Raises: + 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.name == agent_name)) + agent: Agent | None = result.scalars().first() + if agent is None: raise NotFoundError( - error_code=ErrorCode.CONTROL_NOT_FOUND, - detail=f"Control with ID '{control_id}' not found", - resource="Control", - resource_id=str(control_id), - hint="Verify the control ID is correct and the control has been created.", - ) - - validation_errors = _validate_controls_for_agent(agent, [control]) - if validation_errors: - raise BadRequestError( - error_code=ErrorCode.POLICY_CONTROL_INCOMPATIBLE, - detail="Control is incompatible with this agent", - hint="Ensure the control is compatible with this agent's evaluators.", - errors=[ - ValidationErrorItem( - resource="Control", - field="evaluator", - code="incompatible", - message=err, - ) - for err in validation_errors - ], - ) - - try: - stmt = ( - pg_insert(agent_controls) - .values(agent_uuid=agent_id, control_id=control_id) - .on_conflict_do_nothing() - ) - await db.execute(stmt) - await db.commit() - except Exception: - await db.rollback() - _logger.error( - "Failed to associate control '%s' with agent '%s' (%s)", - control_id, - agent.name, - agent_id, - exc_info=True, - ) - raise DatabaseError( - detail=f"Failed to associate control with agent '{agent.name}': database error", + error_code=ErrorCode.AGENT_NOT_FOUND, + detail=f"Agent with name '{agent_name}' not found", resource="Agent", - operation="add control association", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", ) - return AssocResponse(success=True) - - -@router.delete( - "/{agent_id}/controls/{control_id}", - response_model=RemoveAgentControlResponse, - summary="Remove direct control association from agent", - response_description="Success confirmation", -) -async def remove_agent_control( - agent_id: UUID, control_id: int, db: AsyncSession = Depends(get_async_db) -) -> RemoveAgentControlResponse: - """Remove a direct control association from an agent (idempotent).""" - await _get_agent_or_404(agent_id, db) - - control_result = await db.execute(select(Control.id).where(Control.id == control_id)) - if control_result.first() is None: + # Check if agent has a policy + if agent.policy_id is None: raise NotFoundError( - error_code=ErrorCode.CONTROL_NOT_FOUND, - detail=f"Control with ID '{control_id}' not found", - resource="Control", - resource_id=str(control_id), - hint="Verify the control ID is correct and the control has been created.", + error_code=ErrorCode.POLICY_NOT_FOUND, + detail=f"Agent '{agent.name}' has no policy assigned", + resource="Policy", + hint="The agent does not have a policy to remove.", ) + # Remove policy assignment + agent.policy_id = None try: - remove_direct_stmt = ( - delete(agent_controls) - .where( - (agent_controls.c.agent_uuid == agent_id) - & (agent_controls.c.control_id == control_id) - ) - .returning(agent_controls.c.control_id) - ) - remove_direct_result = await db.execute(remove_direct_stmt) - removed_direct_association = remove_direct_result.first() is not None - - # The control may still be active for this agent if inherited from policy association(s). - policy_inheritance_result = await db.execute( - select(policy_controls.c.control_id) - .select_from( - agent_policies.join( - policy_controls, - agent_policies.c.policy_id == policy_controls.c.policy_id, - ) - ) - .where( - (agent_policies.c.agent_uuid == agent_id) - & (policy_controls.c.control_id == control_id) - ) - .limit(1) - ) - control_still_active = policy_inheritance_result.first() is not None - await db.commit() except Exception: await db.rollback() _logger.error( - "Failed to remove control '%s' from agent '%s'", - control_id, - agent_id, + f"Failed to remove policy from agent '{agent.name}' ({agent_name})", exc_info=True, ) raise DatabaseError( - detail=f"Failed to remove control association from agent '{agent_id}': database error", + detail=f"Failed to remove policy from agent '{agent.name}': database error", resource="Agent", - operation="remove control association", + operation="remove policy", ) - return RemoveAgentControlResponse( - success=True, - removed_direct_association=removed_direct_association, - control_still_active=control_still_active, - ) + return DeletePolicyResponse(success=True) @router.get( - "/{agent_id}/controls", + "/{agent_name}/controls", response_model=AgentControlsResponse, summary="List agent's active controls", - response_description="List of controls from agent policy and direct associations", + 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. - Controls include the union of policy-derived and directly associated controls. + Controls are inherited from the agent's assigned policy. + 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: - AgentControlsResponse with list of active controls + AgentControlsResponse with list of controls (empty if no policy) Raises: HTTPException 404: Agent not found """ - await _get_agent_or_404(agent_id, db) + 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 name '{agent_name}' not found", + resource="Agent", + resource_id=str(agent_name), + hint="Verify the agent name is correct and the agent has been registered.", + ) - controls = await list_controls_for_agent(agent_id, db) + if agent.policy_id is None: + return AgentControlsResponse(controls=[]) + + controls = await list_controls_for_agent(agent_name, db) return AgentControlsResponse(controls=controls) @@ -1277,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), @@ -1296,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) @@ -1307,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: @@ -1372,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: @@ -1386,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) @@ -1396,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: @@ -1436,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: @@ -1453,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) @@ -1464,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: @@ -1506,35 +1338,37 @@ async def patch_agent( if request.remove_evaluators: remove_evaluator_set = set(request.remove_evaluators) - # Check if any active controls reference evaluators being removed - controls = await list_controls_for_agent(agent.agent_uuid, db) - referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) - - for ctrl in controls: - evaluator_ref = ctrl.control.evaluator.name - if ":" in evaluator_ref: - ref_agent, ref_eval = evaluator_ref.split(":", 1) - # Check if this control references an evaluator we're removing - # AND it's scoped to this agent (by name match) - if ref_agent == agent.name and ref_eval in remove_evaluator_set: - referencing_controls.append((ctrl.name, ref_eval)) - - if referencing_controls: - raise ConflictError( - error_code=ErrorCode.EVALUATOR_IN_USE, - detail="Cannot remove evaluators: active controls reference them", - resource="Evaluator", - hint="Remove or update the controls that reference these evaluators first.", - errors=[ - ValidationErrorItem( - resource="Control", - field="evaluator.name", - code="in_use", - message=f"Control '{ctrl}' uses evaluator '{ev}'", - ) - for ctrl, ev in referencing_controls - ], - ) + # 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.name, db) + referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) + + for ctrl in controls: + evaluator_ref = ctrl.control.evaluator.name + if ":" in evaluator_ref: + ref_agent, ref_eval = evaluator_ref.split(":", 1) + # Check if this control references an evaluator we're removing + # AND it's scoped to this agent (by name match) + if ref_agent == agent.name and ref_eval in remove_evaluator_set: + referencing_controls.append((ctrl.name, ref_eval)) + + if referencing_controls: + raise ConflictError( + error_code=ErrorCode.EVALUATOR_IN_USE, + detail="Cannot remove evaluators: active controls reference them", + resource="Evaluator", + hint="Remove or update the controls that reference these evaluators first.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator.name", + code="in_use", + message=f"Control '{ctrl}' uses evaluator '{ev}'", + ) + for ctrl, ev in referencing_controls + ], + ) new_evaluators = [] for ev in data_model.evaluators or []: @@ -1556,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 4f274d05..97a85f17 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -21,7 +21,7 @@ from fastapi import APIRouter, Depends, Query from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import ValidationError -from sqlalchemy import delete, func, or_, select, union_all +from sqlalchemy import delete, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -32,12 +32,11 @@ NotFoundError, ) from ..logging_utils import get_logger -from ..models import Agent, AgentData, Control, agent_controls, agent_policies, policy_controls +from ..models import Agent, AgentData, Control, Policy, policy_controls from ..services.evaluator_utils import ( parse_evaluator_ref_full, validate_config_against_schema, ) -from ..services.query_utils import escape_like_pattern # Pagination constants _DEFAULT_PAGINATION_LIMIT = 20 @@ -497,15 +496,19 @@ async def list_controls( Example: GET /controls?limit=10&enabled=true&step_type=tool """ + # Get total count (with filters applied) + count_query = select(func.count()).select_from(Control) query = select(Control).order_by(Control.id.desc()) # Apply cursor if cursor is not None: query = query.where(Control.id < cursor) + count_query = count_query.where(Control.id < cursor) # Apply name filter (case-insensitive partial match) if name is not None: - query = query.where(Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\")) + query = query.where(Control.name.ilike(f"%{name}%")) + # Don't apply to count_query - total should be pre-filter # Apply JSONB filters at database level if enabled is not None: @@ -551,9 +554,7 @@ async def list_controls( # Get total count (with same filters, but without cursor/limit) total_query = select(func.count()).select_from(Control) if name is not None: - total_query = total_query.where( - Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") - ) + total_query = total_query.where(Control.name.ilike(f"%{name}%")) if enabled is not None: if enabled: total_query = total_query.where( @@ -592,51 +593,27 @@ async def list_controls( if has_more: controls = controls[:-1] - # Build mapping of control_id -> usage attribution - # Traversal includes both: - # - Control -> policy_controls -> agent_policies -> Agent - # - Control -> agent_controls -> Agent + # Build mapping of control_id -> agent that uses it + # Traversal: Control -> policy_controls -> Policy -> Agent control_agent_map: dict[int, AgentRef | None] = {ctrl.id: None for ctrl in controls} - control_agent_ids_map: dict[int, set[str]] = {ctrl.id: set() for ctrl in controls} - control_agent_repr_map: dict[int, tuple[str, str] | None] = {ctrl.id: None for ctrl in controls} if controls: control_ids = [ctrl.id for ctrl in controls] - policy_agents_query = ( + agents_query = ( select( policy_controls.c.control_id, - agent_policies.c.agent_uuid, Agent.name, ) .select_from(policy_controls) - .join(agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id) - .join(Agent, Agent.agent_uuid == agent_policies.c.agent_uuid) + .join(Policy, policy_controls.c.policy_id == Policy.id) + .join(Agent, Agent.policy_id == Policy.id) .where(policy_controls.c.control_id.in_(control_ids)) ) - direct_agents_query = ( - select( - agent_controls.c.control_id, - agent_controls.c.agent_uuid, - Agent.name, - ) - .select_from(agent_controls) - .join(Agent, Agent.agent_uuid == agent_controls.c.agent_uuid) - .where(agent_controls.c.control_id.in_(control_ids)) - ) - agents_query = union_all(policy_agents_query, direct_agents_query) agents_result = await db.execute(agents_query) for row in agents_result.all(): - control_id, agent_uuid, agent_name = row - agent_uuid_str = str(agent_uuid) - control_agent_ids_map[control_id].add(agent_uuid_str) - - # Keep a deterministic representative agent for backward compatibility. - current_repr = control_agent_repr_map[control_id] - candidate_repr = (agent_name, agent_uuid_str) - if current_repr is None or candidate_repr < current_repr: - control_agent_repr_map[control_id] = candidate_repr - control_agent_map[control_id] = AgentRef( - agent_id=agent_uuid_str, agent_name=agent_name - ) + 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_name=agent_name) # Build summaries (filtering already done at DB level) summaries: list[ControlSummary] = [] @@ -655,7 +632,6 @@ async def list_controls( stages=scope.get("stages"), tags=data.get("tags", []), used_by_agent=control_agent_map.get(ctrl.id), - used_by_agents_count=len(control_agent_ids_map.get(ctrl.id, set())), ) ) @@ -685,15 +661,15 @@ async def delete_control( control_id: int, force: bool = Query( False, - description="If true, dissociate from all policy/agent links before deleting. " - "If false, fail if control is associated with any policy or agent.", + description="If true, dissociate from all policies before deleting. " + "If false, fail if control is associated with any policy.", ), db: AsyncSession = Depends(get_async_db), ) -> DeleteControlResponse: """ Delete a control by ID. - By default, deletion fails if the control is associated with any policy or agent. + By default, deletion fails if the control is associated with any policy. Use force=true to automatically dissociate and delete. Args: @@ -702,7 +678,7 @@ async def delete_control( db: Database session (injected) Returns: - DeleteControlResponse with success flag and dissociation details + DeleteControlResponse with success flag and list of dissociated policies Raises: HTTPException 404: Control not found @@ -721,66 +697,48 @@ async def delete_control( hint="Verify the control ID is correct and the control has been created.", ) - # Check for associations with policies and direct agent links - policy_assoc_result = await db.execute( - select(policy_controls.c.policy_id).where(policy_controls.c.control_id == control_id) + # Check for associations with policies + assoc_result = await db.execute( + select(policy_controls.c.policy_id).where( + policy_controls.c.control_id == control_id + ) ) - associated_policy_ids = [row[0] for row in policy_assoc_result.all()] + associated_policy_ids = [row[0] for row in assoc_result.all()] - agent_assoc_result = await db.execute( - select(agent_controls.c.agent_uuid).where(agent_controls.c.control_id == control_id) - ) - associated_agent_ids = [str(row[0]) for row in agent_assoc_result.all()] - - if (associated_policy_ids or associated_agent_ids) and not force: - errors = [ - ValidationErrorItem( - resource="Policy", - field="controls", - code="control_in_use", - message=f"Control is associated with policy ID {pid}", - value=pid, - ) - for pid in associated_policy_ids - ] + [ - ValidationErrorItem( - resource="Agent", - field="controls", - code="control_in_use", - message=f"Control is directly associated with agent ID {agent_id}", - value=agent_id, - ) - for agent_id in associated_agent_ids - ] + if associated_policy_ids and not force: raise ConflictError( error_code=ErrorCode.CONTROL_IN_USE, detail=( f"Control '{control.name}' is associated with " - f"{len(associated_policy_ids)} policy/policies and " - f"{len(associated_agent_ids)} agent(s)" + f"{len(associated_policy_ids)} policy/policies" ), resource="Control", resource_id=control.name, hint="Use force=true to dissociate and delete, or remove associations manually first.", - errors=errors, + errors=[ + ValidationErrorItem( + resource="Policy", + field="controls", + code="control_in_use", + message=f"Control is associated with policy ID {pid}", + value=pid, + ) + for pid in associated_policy_ids + ], ) # Remove associations if force=true - dissociated_from_policies: list[int] = [] - dissociated_from_agents: list[str] = [] + dissociated_from: list[int] = [] if associated_policy_ids: - await db.execute(delete(policy_controls).where(policy_controls.c.control_id == control_id)) - dissociated_from_policies = associated_policy_ids - if associated_agent_ids: - await db.execute(delete(agent_controls).where(agent_controls.c.control_id == control_id)) - dissociated_from_agents = associated_agent_ids - if dissociated_from_policies or dissociated_from_agents: + await db.execute( + delete(policy_controls).where( + policy_controls.c.control_id == control_id + ) + ) + dissociated_from = associated_policy_ids _logger.info( - "Dissociated control '%s' (%s) from %s policy/policies and %s agent(s)", - control.name, - control_id, - len(dissociated_from_policies), - len(dissociated_from_agents), + f"Dissociated control '{control.name}' ({control_id}) " + f"from {len(dissociated_from)} policy/policies" ) # Delete the control @@ -800,11 +758,7 @@ async def delete_control( operation="delete", ) - return DeleteControlResponse( - success=True, - dissociated_from_policies=dissociated_from_policies, - dissociated_from_agents=dissociated_from_agents, - ) + return DeleteControlResponse(success=True, dissociated_from=dissociated_from) @router.patch( 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 874086bb..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 +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 @@ -39,31 +38,13 @@ class AgentData(BaseModel): Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), ) -# Association table for Agent <> Policy many-to-many relationship -agent_policies: Table = Table( - "agent_policies", - Base.metadata, - Column("agent_uuid", ForeignKey("agents.agent_uuid"), primary_key=True, index=True), - Column("policy_id", ForeignKey("policies.id"), primary_key=True, index=True), -) - -# Association table for Agent <> Control many-to-many direct relationship -agent_controls: Table = Table( - "agent_controls", - Base.metadata, - Column("agent_uuid", ForeignKey("agents.agent_uuid"), primary_key=True, index=True), - Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), -) - class Policy(Base): __tablename__ = "policies" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) - agents: Mapped[list["Agent"]] = relationship( - "Agent", secondary=lambda: agent_policies, back_populates="policies" - ) + agents: Mapped[list["Agent"]] = relationship("Agent", back_populates="policy") # Many-to-many: Policy <> Control (direct relationship, no ControlSet layer) controls: Mapped[list["Control"]] = relationship( "Control", secondary=lambda: policy_controls, back_populates="policies" @@ -83,10 +64,6 @@ class Control(Base): policies: Mapped[list["Policy"]] = relationship( "Policy", secondary=lambda: policy_controls, back_populates="controls" ) - # Many-to-many backref: Control <> Agent (direct relationship) - agents: Mapped[list["Agent"]] = relationship( - "Agent", secondary=lambda: agent_controls, back_populates="controls" - ) class EvaluatorConfigDB(Base): @@ -111,24 +88,27 @@ 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 ) - policies: Mapped[list["Policy"]] = relationship( - "Policy", secondary=lambda: agent_policies, back_populates="agents" - ) - controls: Mapped[list["Control"]] = relationship( - "Control", secondary=lambda: agent_controls, back_populates="agents" + policy_id: Mapped[int | None] = mapped_column( + ForeignKey("policies.id"), nullable=True, index=True ) + policy: Mapped[Optional["Policy"]] = relationship("Policy", back_populates="agents") created_at: Mapped[dt.datetime] = mapped_column( 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 @@ -140,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. """ @@ -162,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( @@ -173,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 42bafac7..985ca7b5 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, @@ -88,7 +87,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 @@ -113,7 +112,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: @@ -134,7 +133,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), }) @@ -143,9 +142,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 @@ -159,7 +158,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, @@ -174,7 +173,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 @@ -187,7 +186,7 @@ async def query_stats( cutoff = now - time_range params: dict = { - "agent_uuid": agent_uuid, + "agent_name": agent_name, "cutoff": cutoff, } @@ -208,7 +207,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} ), @@ -279,7 +278,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' @@ -393,11 +392,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) @@ -414,9 +413,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 929c103e..46f9987d 100644 --- a/server/src/agent_control_server/services/controls.py +++ b/server/src/agent_control_server/services/controls.py @@ -2,17 +2,16 @@ 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 from agent_control_models.policy import Control as APIControl from pydantic import ValidationError -from sqlalchemy import select, union +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ..errors import APIValidationError -from ..models import Control, agent_controls, agent_policies, policy_controls +from ..models import Agent, Control, Policy, policy_controls _logger = logging.getLogger(__name__) @@ -29,35 +28,24 @@ 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, ) -> list[APIControl]: - """Return API Control models for controls associated with the agent. + """Return API Control models for all configured controls associated with the agent's policy. - Active controls are the de-duplicated union of: - - controls inherited from all assigned policies - - controls directly associated with the agent + Traversal: Agent -> Policy -> Controls (direct relationship). + Uses explicit joins over association table to avoid async relationship loading. Note: Invalid ControlDefinition data triggers an APIValidationError. """ - policy_control_ids = ( - select(policy_controls.c.control_id.label("control_id")) - .select_from( - policy_controls.join( - agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id - ) - ) - .where(agent_policies.c.agent_uuid == agent_id) - ) - direct_control_ids = select(agent_controls.c.control_id.label("control_id")).where( - agent_controls.c.agent_uuid == agent_id - ) - control_ids_subquery = union(policy_control_ids, direct_control_ids).subquery() - - stmt = select(Control).join( - control_ids_subquery, Control.id == control_ids_subquery.c.control_id + stmt = ( + select(Control) + .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.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 25b9ff75..2297e9a5 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -7,8 +7,8 @@ from fastapi.testclient import TestClient from sqlalchemy import text -from .conftest import engine from .utils import VALID_CONTROL_PAYLOAD +from .conftest import engine def _init_agent( @@ -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: @@ -230,7 +230,7 @@ def test_patch_agent_remove_evaluator_in_use_conflict(client: TestClient) -> Non policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign.status_code == 200 # When: attempting to remove evaluator in use @@ -270,52 +270,19 @@ def test_set_agent_policy_incompatible_controls(client: TestClient) -> None: agent_b_id, _ = _init_agent(client) # When: assigning policy to agent B - resp = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_b_id}/policy/{policy_id}") # Then: incompatible controls error assert resp.status_code == 400 assert resp.json()["error_code"] == "POLICY_CONTROL_INCOMPATIBLE" -def test_add_agent_control_incompatible_with_agent_returns_400(client: TestClient) -> None: - # Given: Agent A exposes a custom evaluator - evaluators = [ - { - "name": "custom", - "description": "custom", - "config_schema": {"type": "object", "properties": {}, "additionalProperties": True}, - } - ] - _, agent_a_name = _init_agent(client, evaluators=evaluators) - - # And: control references Agent A's evaluator - control_payload = deepcopy(VALID_CONTROL_PAYLOAD) - control_payload["evaluator"] = { - "name": f"{agent_a_name}:custom", - "config": {}, - } - control_id = _create_control_with_data(client, control_payload) - - # And: Agent B does not expose that evaluator - agent_b_id, _ = _init_agent(client) - - # When: associating the control directly with Agent B - resp = client.post(f"/api/v1/agents/{agent_b_id}/controls/{control_id}") - - # Then: validation fails with compatibility error - assert resp.status_code == 400 - body = resp.json() - assert body["error_code"] == "POLICY_CONTROL_INCOMPATIBLE" - assert body["errors"] - assert all(err.get("field") == "evaluator" for err in body["errors"]) - - def test_init_agent_rejects_builtin_evaluator_name(client: TestClient) -> None: # Given: a payload that registers an evaluator matching a built-in name 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", }, @@ -333,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", @@ -351,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", @@ -374,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( @@ -390,7 +353,7 @@ def test_list_agent_controls_corrupted_control_data_returns_422( policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign.status_code == 200 # And: the control data is corrupted in the DB @@ -410,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") @@ -433,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}, ) @@ -457,12 +420,12 @@ 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}, ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -491,7 +454,7 @@ def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -533,7 +496,7 @@ def test_set_agent_policy_rejects_invalid_agent_evaluator_config(client: TestCli ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -547,7 +510,7 @@ def test_get_agent_policy_agent_not_found(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: retrieving policy for a non-existent agent - resp = client.get(f"/api/v1/agents/{missing_agent}/policies") + resp = client.get(f"/api/v1/agents/{missing_agent}/policy") # Then: not found error is returned assert resp.status_code == 404 @@ -559,74 +522,19 @@ def test_delete_agent_policy_agent_not_found(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: deleting policy for a non-existent agent - resp = client.delete(f"/api/v1/agents/{missing_agent}/policies") + resp = client.delete(f"/api/v1/agents/{missing_agent}/policy") # Then: not found error is returned assert resp.status_code == 404 assert resp.json()["error_code"] == "AGENT_NOT_FOUND" -def test_delete_agent_policy_no_policy_assigned_is_idempotent(client: TestClient) -> None: +def test_delete_agent_policy_no_policy_assigned_returns_404(client: TestClient) -> None: # Given: an agent with no policy assigned agent_id, _ = _init_agent(client) # When: deleting policy - resp = client.delete(f"/api/v1/agents/{agent_id}/policies") - - # Then: deletion is idempotent - assert resp.status_code == 200 - assert resp.json()["success"] is True - - -def test_remove_agent_policy_removes_only_target_association(client: TestClient) -> None: - # Given: an agent associated with two policies - agent_id, _ = _init_agent(client) - policy_a_id = _create_policy(client) - policy_b_id = _create_policy(client) - - assoc_a = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") - assert assoc_a.status_code == 200 - assoc_b = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") - assert assoc_b.status_code == 200 - - # When: removing one specific policy association - remove_resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") - - # Then: only that policy association is removed - assert remove_resp.status_code == 200 - assert remove_resp.json()["success"] is True - get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") - assert get_resp.status_code == 200 - assert get_resp.json()["policy_ids"] == [policy_b_id] - - -def test_remove_agent_policy_non_associated_existing_policy_is_noop(client: TestClient) -> None: - # Given: an agent associated with policy A, but policy B exists and is not associated - agent_id, _ = _init_agent(client) - policy_a_id = _create_policy(client) - policy_b_id = _create_policy(client) - - assoc_a = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") - assert assoc_a.status_code == 200 - - # When: removing non-associated policy B - remove_resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") - - # Then: operation succeeds and existing association remains - assert remove_resp.status_code == 200 - assert remove_resp.json()["success"] is True - get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") - assert get_resp.status_code == 200 - assert get_resp.json()["policy_ids"] == [policy_a_id] - - -def test_remove_agent_policy_missing_policy_returns_404(client: TestClient) -> None: - # Given: an existing agent and a non-existent policy id - agent_id, _ = _init_agent(client) - missing_policy_id = 999999999 - - # When: removing a missing policy association - resp = client.delete(f"/api/v1/agents/{agent_id}/policies/{missing_policy_id}") + resp = client.delete(f"/api/v1/agents/{agent_id}/policy") # Then: policy not found error is returned assert resp.status_code == 404 @@ -642,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}, ) @@ -651,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 @@ -662,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}, ) @@ -680,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}, ) @@ -692,16 +600,53 @@ def test_get_agent_corrupted_metadata_returns_422(client: TestClient) -> None: assert resp.json()["error_code"] == "CORRUPTED_DATA" -def test_get_agent_policies_returns_empty_when_none_assigned(client: TestClient) -> None: - # Given: an agent with no policy assignments +def test_get_agent_policy_missing_policy_returns_404(client: TestClient) -> None: + # Given: an agent assigned to a policy that cannot be found agent_id, _ = _init_agent(client) + policy_id = _create_policy(client) + assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assert assign.status_code == 200 - # When: retrieving associated policies - resp = client.get(f"/api/v1/agents/{agent_id}/policies") + from agent_control_server.db import get_async_db + from agent_control_server.main import app + from agent_control_server.models import Agent as AgentModel + from sqlalchemy.orm import Session + from unittest.mock import AsyncMock, MagicMock + from collections.abc import AsyncGenerator + from sqlalchemy.ext.asyncio import AsyncSession + from sqlalchemy import select + + with Session(engine) as session: + agent_row = ( + session.execute( + select(AgentModel).where(AgentModel.name == agent_id) + ) + .scalars() + .first() + ) + assert agent_row is not None + + async def mock_db_missing_policy() -> AsyncGenerator[AsyncSession, None]: + mock_session = AsyncMock(spec=AsyncSession) + mock_agent_result = MagicMock() + mock_agent_result.scalars.return_value.first.return_value = agent_row + mock_policy_result = MagicMock() + mock_policy_result.scalars.return_value.first.return_value = None + mock_session.execute = AsyncMock( + side_effect=[mock_agent_result, mock_policy_result] + ) + yield mock_session - # Then: an empty policy list is returned - assert resp.status_code == 200 - assert resp.json()["policy_ids"] == [] + # When: retrieving the agent policy and policy lookup returns None + app.dependency_overrides[get_async_db] = mock_db_missing_policy + try: + resp = client.get(f"/api/v1/agents/{agent_id}/policy") + finally: + app.dependency_overrides.clear() + + # Then: policy not found error is returned + assert resp.status_code == 404 + assert resp.json()["error_code"] == "POLICY_NOT_FOUND" def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> None: @@ -715,7 +660,7 @@ def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> Non assert assoc.status_code == 200 # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: assignment succeeds because empty data is ignored during validation assert resp.status_code == 200 @@ -737,7 +682,7 @@ def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClie ) # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: assignment succeeds because evaluator name is missing assert resp.status_code == 200 @@ -755,7 +700,7 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: for control_id in control_ids: assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign.status_code == 200 # When: listing agents @@ -767,34 +712,10 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: assert agent["active_controls_count"] == 2 -def test_list_agents_active_controls_count_deduplicates_policy_and_direct( - client: TestClient, -) -> None: - # Given: an agent with the same control linked through both policy and direct association - agent_id, _ = _init_agent(client) - policy_id = _create_policy(client) - shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) - - assoc_policy_control = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") - assert assoc_policy_control.status_code == 200 - assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - assert assign_policy.status_code == 200 - assoc_direct_control = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") - assert assoc_direct_control.status_code == 200 - - # When: listing agents - resp = client.get("/api/v1/agents") - - # Then: active_controls_count counts the shared control once - assert resp.status_code == 200 - agent = next(a for a in resp.json()["agents"] if a["agent_id"] == agent_id) - assert agent["active_controls_count"] == 1 - - 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}) @@ -813,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, @@ -852,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={ @@ -873,7 +794,7 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign.status_code == 200 # When: re-initializing the agent with the same UUID @@ -898,90 +819,12 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> assert controls[0]["id"] == control_id -def test_init_agent_returns_union_and_deduplicates_policy_and_direct_controls( - client: TestClient, -) -> None: - # Given: an agent with policy-only, direct-only, and shared control associations - agent_id, agent_name = _init_agent(client) - policy_id = _create_policy(client) - - policy_only_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) - direct_only_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) - shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) - - assoc_policy_only = client.post( - f"/api/v1/policies/{policy_id}/controls/{policy_only_control_id}" - ) - assert assoc_policy_only.status_code == 200 - assoc_shared_policy = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") - assert assoc_shared_policy.status_code == 200 - assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - assert assign_policy.status_code == 200 - - assoc_direct_only = client.post(f"/api/v1/agents/{agent_id}/controls/{direct_only_control_id}") - assert assoc_direct_only.status_code == 200 - assoc_shared_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") - assert assoc_shared_direct.status_code == 200 - - # When: re-initializing the same agent - reinit_resp = client.post( - "/api/v1/agents/initAgent", - json={ - "agent": { - "agent_id": agent_id, - "agent_name": agent_name, - "agent_description": "desc", - "agent_version": "1.0", - }, - "steps": [], - "evaluators": [], - }, - ) - - # Then: initAgent returns the union of controls with shared control de-duplicated - assert reinit_resp.status_code == 200 - body = reinit_resp.json() - assert body["created"] is False - returned_control_ids = [control["id"] for control in body["controls"]] - assert set(returned_control_ids) == { - policy_only_control_id, - direct_only_control_id, - shared_control_id, - } - assert len(returned_control_ids) == 3 - - -def test_policy_removal_does_not_remove_direct_association_for_same_control( - client: TestClient, -) -> None: - # Given: an agent where the same control is linked both via policy and directly - agent_id, _ = _init_agent(client) - policy_id = _create_policy(client) - shared_control_id = _create_control_with_data(client, deepcopy(VALID_CONTROL_PAYLOAD)) - - assoc_control = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") - assert assoc_control.status_code == 200 - assign_policy = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - assert assign_policy.status_code == 200 - assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{shared_control_id}") - assert assoc_direct.status_code == 200 - - # When: removing the policy association from the agent - remove_policy = client.delete(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - - # Then: the control is still active through the direct association - assert remove_policy.status_code == 200 - list_controls = client.get(f"/api/v1/agents/{agent_id}/controls") - assert list_controls.status_code == 200 - assert {control["id"] for control in list_controls.json()["controls"]} == {shared_control_id} - - def test_patch_agent_corrupted_data_returns_422(client: TestClient) -> None: # Given: an agent with corrupted stored data 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}, ) @@ -1001,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}, ) @@ -1020,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", }, @@ -1048,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, @@ -1102,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 1a944fed..4be88a31 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -6,13 +6,16 @@ from types import SimpleNamespace import pytest -from agent_control_evaluators import RegexEvaluatorConfig -from agent_control_server.endpoints import controls as controls_module -from agent_control_server.models import Control from fastapi.testclient import TestClient from sqlalchemy import text from sqlalchemy.orm import Session +from agent_control_server.models import Control + +from agent_control_evaluators import RegexEvaluatorConfig +from agent_control_server.endpoints import controls as controls_module +from agent_control_server.models import Control + from .conftest import engine from .utils import VALID_CONTROL_PAYLOAD @@ -29,24 +32,6 @@ def _set_control_data(client: TestClient, control_id: int, data: dict) -> None: assert resp.status_code == 200, resp.text -def _init_agent(client: TestClient, name: str | None = None) -> str: - agent_id = str(uuid.uuid4()) - payload = { - "agent": { - "agent_id": agent_id, - "agent_name": name or f"agent-{uuid.uuid4()}", - "agent_description": "test", - "agent_version": "1.0", - "agent_metadata": {}, - }, - "steps": [], - "evaluators": [], - } - resp = client.post("/api/v1/agents/initAgent", json=payload) - assert resp.status_code == 200 - return agent_id - - def test_list_controls_filters_and_pagination(client: TestClient) -> None: # Given: three controls with varying data control1_id, control1_name = _create_control(client, name=f"AlphaControl-{uuid.uuid4()}") @@ -141,40 +126,6 @@ def test_list_controls_filters_and_pagination(client: TestClient) -> None: assert page2["controls"][0]["id"] != first_id -def test_list_controls_includes_unique_used_by_agents_count(client: TestClient) -> None: - # Given: one control linked to two agents via policy and one duplicated direct link - control_id, control_name = _create_control(client, name=f"CountedControl-{uuid.uuid4()}") - _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) - - policy_resp = client.put("/api/v1/policies", json={"name": f"policy-{uuid.uuid4()}"}) - assert policy_resp.status_code == 200 - policy_id = policy_resp.json()["policy_id"] - - assoc_control = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - assert assoc_control.status_code == 200 - - agent_a_id = _init_agent(client, name=f"agent-a-{uuid.uuid4()}") - agent_b_id = _init_agent(client, name=f"agent-b-{uuid.uuid4()}") - - assoc_policy_a = client.post(f"/api/v1/agents/{agent_a_id}/policies/{policy_id}") - assoc_policy_b = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") - assert assoc_policy_a.status_code == 200 - assert assoc_policy_b.status_code == 200 - - # Duplicate path for agent A: direct + policy should still count as one unique agent - assoc_direct_a = client.post(f"/api/v1/agents/{agent_a_id}/controls/{control_id}") - assert assoc_direct_a.status_code == 200 - - # When: listing controls - list_resp = client.get("/api/v1/controls", params={"name": control_name}) - assert list_resp.status_code == 200 - controls = list_resp.json()["controls"] - assert len(controls) == 1 - - # Then: used-by count is de-duplicated at agent level - assert controls[0]["used_by_agents_count"] == 2 - - def test_patch_control_enabled_requires_data(client: TestClient) -> None: # Given: a control without configured data control_id, _ = _create_control(client) @@ -284,31 +235,11 @@ def test_list_controls_combined_filters(client: TestClient) -> None: assert names == [control1_name] -def test_list_controls_name_filter_treats_wildcards_as_literals(client: TestClient) -> None: - # Given: two controls where one only matches if '_' is interpreted as wildcard - literal_name = f"Control_{uuid.uuid4().hex[:6]}" - wildcard_match_name = literal_name.replace("_", "X", 1) - - _, created_literal_name = _create_control(client, name=literal_name) - _create_control(client, name=wildcard_match_name) - - # When: filtering by the literal name containing '_' - resp = client.get("/api/v1/controls", params={"name": literal_name}) - assert resp.status_code == 200 - names = [control["name"] for control in resp.json()["controls"]] - - # Then: only exact literal '_' matches are returned - assert names == [created_literal_name] - assert resp.json()["pagination"]["total"] == 1 - - def test_list_controls_enabled_true_includes_missing_enabled(client: TestClient) -> None: # Given: controls with enabled true, enabled false, and missing enabled control_true_id, control_true_name = _create_control(client, name=f"Enabled-{uuid.uuid4()}") control_false_id, control_false_name = _create_control(client, name=f"Disabled-{uuid.uuid4()}") - control_missing_id, control_missing_name = _create_control( - client, name=f"Missing-{uuid.uuid4()}" - ) + control_missing_id, control_missing_name = _create_control(client, name=f"Missing-{uuid.uuid4()}") data_true = deepcopy(VALID_CONTROL_PAYLOAD) data_true["enabled"] = True @@ -427,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) @@ -451,8 +417,7 @@ def test_delete_control_force_dissociates(client: TestClient) -> None: assert resp2.status_code == 200 body = resp2.json() assert body["success"] is True - assert policy_id in body.get("dissociated_from_policies", []) - assert body.get("dissociated_from_agents") == [] + assert policy_id in body.get("dissociated_from", []) # Then: policy no longer lists the control list_resp = client.get(f"/api/v1/policies/{policy_id}/controls") @@ -460,73 +425,6 @@ def test_delete_control_force_dissociates(client: TestClient) -> None: assert control_id not in list_resp.json()["control_ids"] -def test_delete_control_force_dissociates_from_policy_and_direct(client: TestClient) -> None: - # Given: one control associated both to a policy and directly to an agent - control_id, _ = _create_control(client) - _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) - - policy_resp = client.put("/api/v1/policies", json={"name": f"policy-{uuid.uuid4()}"}) - assert policy_resp.status_code == 200 - policy_id = policy_resp.json()["policy_id"] - assoc_policy = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - assert assoc_policy.status_code == 200 - - agent_id = _init_agent(client, name=f"agent-{uuid.uuid4()}") - assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") - assert assoc_direct.status_code == 200 - - # When: deleting with force - delete_resp = client.delete(f"/api/v1/controls/{control_id}?force=true") - - # Then: response reports dissociation from both paths - assert delete_resp.status_code == 200 - body = delete_resp.json() - assert body["success"] is True - assert body["dissociated_from_policies"] == [policy_id] - assert body["dissociated_from_agents"] == [agent_id] - - # Then: policy and agent no longer expose the control - list_policy_controls = client.get(f"/api/v1/policies/{policy_id}/controls") - assert list_policy_controls.status_code == 200 - assert control_id not in list_policy_controls.json()["control_ids"] - - list_agent_controls = client.get(f"/api/v1/agents/{agent_id}/controls") - assert list_agent_controls.status_code == 200 - assert control_id not in {control["id"] for control in list_agent_controls.json()["controls"]} - - # Then: the control resource is deleted globally - get_control = client.get(f"/api/v1/controls/{control_id}") - assert get_control.status_code == 404 - - -def test_delete_control_without_force_blocked_by_direct_association( - client: TestClient, -) -> None: - # Given: a control directly associated to an agent - control_id, _ = _create_control(client) - _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) - - agent_id = _init_agent(client, name=f"agent-{uuid.uuid4()}") - assoc_direct = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") - assert assoc_direct.status_code == 200 - - # When: deleting without force - delete_resp = client.delete(f"/api/v1/controls/{control_id}") - - # Then: delete is rejected as control-in-use by agent association - assert delete_resp.status_code == 409 - body = delete_resp.json() - assert body["error_code"] == "CONTROL_IN_USE" - assert any( - err.get("resource") == "Agent" - and ( - err.get("value") == agent_id - or agent_id in err.get("message", "") - ) - for err in body.get("errors", []) - ) - - def test_get_control_corrupted_data_returns_none(client: TestClient) -> None: # Given: a control with corrupted data in DB control_id, control_name = _create_control(client) @@ -600,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={ @@ -628,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={ @@ -718,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={ @@ -732,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 26aca114..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,14 +73,14 @@ 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}) assert r2.status_code == 200 policy_id = r2.json()["policy_id"] - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign_resp.status_code == 200 # And: a database session that fails on commit @@ -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 @@ -105,7 +106,7 @@ async def mock_db_for_delete_policy() -> AsyncGenerator[AsyncSession, None]: # When: deleting policy and commit fails app.dependency_overrides[get_async_db] = mock_db_for_delete_policy try: - resp = client.delete(f"/api/v1/agents/{agent_id}/policies") + resp = client.delete(f"/api/v1/agents/{agent_id}/policy") finally: app.dependency_overrides.clear() @@ -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 @@ -320,12 +321,10 @@ async def mock_db_for_delete_control() -> AsyncGenerator[AsyncSession, None]: control_result = MagicMock() control_result.scalars.return_value.first.return_value = existing_control - policy_assoc_result = MagicMock() - policy_assoc_result.all.return_value = [] - agent_assoc_result = MagicMock() - agent_assoc_result.all.return_value = [] + assoc_result = MagicMock() + assoc_result.all.return_value = [] mock_session.execute = AsyncMock( - side_effect=[control_result, policy_assoc_result, agent_assoc_result] + side_effect=[control_result, assoc_result] ) mock_session.delete = AsyncMock() mock_session.rollback = AsyncMock() @@ -360,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}) @@ -374,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 = ( @@ -417,7 +416,7 @@ async def mock_db_for_policy_assignment() -> AsyncGenerator[AsyncSession, None]: app.dependency_overrides[get_async_db] = mock_db_for_policy_assignment try: - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: rollback is called and 500 error is returned assert resp.status_code == 500 diff --git a/server/tests/test_evaluation_e2e.py b/server/tests/test_evaluation_e2e.py index 66a4e206..8d3f6ad1 100644 --- a/server/tests/test_evaluation_e2e.py +++ b/server/tests/test_evaluation_e2e.py @@ -1,9 +1,7 @@ """End-to-end tests for evaluation flow.""" import uuid - -from agent_control_models import EvaluationRequest, Step from fastapi.testclient import TestClient - +from agent_control_models import EvaluationRequest, Step from .utils import create_and_assign_policy @@ -21,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" ) @@ -43,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" ) @@ -63,54 +61,6 @@ def test_evaluation_no_policy(client: TestClient): assert not resp.json()["matches"] -def test_evaluation_uses_direct_controls_without_policy(client: TestClient): - # Given: an agent with no policy and a direct deny control attached - agent_uuid = uuid.uuid4() - init_resp = client.post( - "/api/v1/agents/initAgent", - json={ - "agent": {"agent_id": str(agent_uuid), "agent_name": "DirectControlAgent"}, - "steps": [], - }, - ) - assert init_resp.status_code == 200 - - control_name = f"control-{uuid.uuid4()}" - create_control = client.put("/api/v1/controls", json={"name": control_name}) - assert create_control.status_code == 200 - control_id = create_control.json()["control_id"] - - control_data = { - "description": "Direct deny control", - "enabled": True, - "execution": "server", - "scope": {"step_types": ["llm"], "stages": ["pre"]}, - "selector": {"path": "input"}, - "evaluator": {"name": "regex", "config": {"pattern": "secret"}}, - "action": {"decision": "deny"}, - } - set_data = client.put(f"/api/v1/controls/{control_id}/data", json={"data": control_data}) - assert set_data.status_code == 200 - - add_direct = client.post(f"/api/v1/agents/{str(agent_uuid)}/controls/{control_id}") - assert add_direct.status_code == 200 - - # When: evaluating input that matches the direct control - req = EvaluationRequest( - agent_uuid=agent_uuid, - step=Step(type="llm", name="test-step", input="contains a secret", output=None), - stage="pre", - ) - eval_resp = client.post("/api/v1/evaluation", json=req.model_dump(mode="json")) - - # Then: evaluation is denied by the direct control even without any policy - assert eval_resp.status_code == 200 - body = eval_resp.json() - assert body["is_safe"] is False - assert body["matches"] is not None - assert any(match["control_name"] == control_name for match in body["matches"]) - - def test_evaluation_empty_policy(client: TestClient): """Test that an agent with an empty policy is safe.""" # Given: an empty policy @@ -119,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)}/policies/{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" ) @@ -156,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" ) @@ -185,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) @@ -213,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 @@ -223,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" ) @@ -241,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" ) @@ -267,14 +217,12 @@ 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}/policies") - policy_ids = resp.json()["policy_ids"] - assert len(policy_ids) == 1 - policy_id = policy_ids[0] + resp = client.get(f"/api/v1/agents/{agent_name}/policy") + policy_id = resp.json()["policy_id"] # Create Deny Control control_deny = { @@ -295,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" ) @@ -323,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"), @@ -340,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" ) @@ -362,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" ) @@ -377,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" ) @@ -401,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" ) @@ -415,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 655eec6e..2bc46e5e 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -1,25 +1,30 @@ +import json import logging import uuid from typing import Any -from agent_control_server.config import db_config -from agent_control_server.models import Agent +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from sqlalchemy import create_engine, select +from sqlalchemy import create_engine, select, text from sqlalchemy.orm import Session +from agent_control_server.config import db_config +from agent_control_server.models import Agent + # Create sync engine for raw database queries in tests engine = create_engine(db_config.get_url(), echo=False) 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 = [ { @@ -31,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"}, @@ -48,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: @@ -62,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() @@ -97,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"}, @@ -115,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"}, @@ -128,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" @@ -262,6 +307,8 @@ def test_init_agent_logs_warning_on_bad_existing_data(client: TestClient, caplog assert any("Failed to parse existing agent data" in m for m in messages) +import uuid + def _create_policy(client: TestClient) -> int: # Helper: create a policy via API and return id name = f"pol-{uuid.uuid4()}" @@ -281,11 +328,12 @@ def test_set_agent_policy_first_time(client: TestClient) -> None: agent_id = payload["agent"]["agent_id"] # When: assigning policy the first time - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - # Then: success + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + # Then: success and no old policy assert resp.status_code == 200 body = resp.json() assert body["success"] is True + assert body["old_policy_id"] is None def test_get_agent_policy_after_assignment(client: TestClient) -> None: @@ -294,32 +342,30 @@ def test_get_agent_policy_after_assignment(client: TestClient) -> None: payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # When: retrieving policy - resp = client.get(f"/api/v1/agents/{agent_id}/policies") - # Then: we see the assigned policy id in the policy_ids list + resp = client.get(f"/api/v1/agents/{agent_id}/policy") + # Then: we see the assigned policy id assert resp.status_code == 200 - assert resp.json()["policy_ids"] == [policy_id] + assert resp.json()["policy_id"] == policy_id -def test_adding_second_policy_retains_existing_policy(client: TestClient) -> None: +def test_reassign_agent_policy_returns_old_id(client: TestClient) -> None: # Given: an agent with an existing policy first = _create_policy(client) second = _create_policy(client) payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policies/{first}") + client.post(f"/api/v1/agents/{agent_id}/policy/{first}") - # When: adding another policy - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{second}") - # Then: success and both policy associations are retained + # When: reassigning to another policy + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{second}") + # Then: success and old_policy_id equals the first policy id assert resp.status_code == 200 assert resp.json()["success"] is True - get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") - assert get_resp.status_code == 200 - assert get_resp.json()["policy_ids"] == [first, second] + assert resp.json()["old_policy_id"] == first def test_delete_agent_policy_then_get_404(client: TestClient) -> None: @@ -328,19 +374,18 @@ def test_delete_agent_policy_then_get_404(client: TestClient) -> None: payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # When: removing the policy association - del_resp = client.delete(f"/api/v1/agents/{agent_id}/policies") + del_resp = client.delete(f"/api/v1/agents/{agent_id}/policy") # Then: deletion success assert del_resp.status_code == 200 assert del_resp.json()["success"] is True - # When: fetching policies after deletion - get_resp = client.get(f"/api/v1/agents/{agent_id}/policies") - # Then: empty policy list - assert get_resp.status_code == 200 - assert get_resp.json()["policy_ids"] == [] + # When: fetching policy after deletion + get_resp = client.get(f"/api/v1/agents/{agent_id}/policy") + # Then: not found + assert get_resp.status_code == 404 def test_set_policy_agent_not_found_returns_404(client: TestClient) -> None: @@ -349,7 +394,7 @@ def test_set_policy_agent_not_found_returns_404(client: TestClient) -> None: missing_agent = str(uuid.uuid4()) # When: assigning to missing agent - resp = client.post(f"/api/v1/agents/{missing_agent}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{missing_agent}/policy/{policy_id}") # Then: 404 assert resp.status_code == 404 @@ -362,7 +407,7 @@ def test_set_policy_not_found_returns_404(client: TestClient) -> None: bogus_policy = "999999999" # When: assigning a non-existent policy - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{bogus_policy}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{bogus_policy}") # Then: 404 assert resp.status_code == 404 @@ -402,7 +447,7 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: # Associate control -> policy; assign policy to agent client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # When: listing controls r = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -412,106 +457,11 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: assert isinstance(body.get("controls"), list) # Verify control data is present and matches description assert any( - item.get("control", {}).get("description") == data_payload["description"] + item.get("control", {}).get("description") == data_payload["description"] for item in body["controls"] ) -def test_add_and_remove_direct_agent_control_does_not_delete_global_control( - client: TestClient, -) -> None: - # Given: an agent and a configured control - payload = make_agent_payload() - init_resp = client.post("/api/v1/agents/initAgent", json=payload) - assert init_resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] - - ctl_name = f"control-{uuid.uuid4()}" - ctl = client.put("/api/v1/controls", json={"name": ctl_name}) - assert ctl.status_code == 200 - control_id = ctl.json()["control_id"] - - from .utils import VALID_CONTROL_PAYLOAD - - set_data_resp = client.put( - f"/api/v1/controls/{control_id}/data", json={"data": VALID_CONTROL_PAYLOAD} - ) - assert set_data_resp.status_code == 200 - - # When: associating control directly with the agent - add_resp = client.post(f"/api/v1/agents/{agent_id}/controls/{control_id}") - assert add_resp.status_code == 200 - assert add_resp.json()["success"] is True - - list_resp = client.get(f"/api/v1/agents/{agent_id}/controls") - assert list_resp.status_code == 200 - assert {item["id"] for item in list_resp.json()["controls"]} == {control_id} - - # When: removing direct control association from this agent - remove_resp = client.delete(f"/api/v1/agents/{agent_id}/controls/{control_id}") - assert remove_resp.status_code == 200 - assert remove_resp.json()["success"] is True - assert remove_resp.json()["removed_direct_association"] is True - assert remove_resp.json()["control_still_active"] is False - - # Then: agent no longer has the control - post_remove_list = client.get(f"/api/v1/agents/{agent_id}/controls") - assert post_remove_list.status_code == 200 - assert post_remove_list.json()["controls"] == [] - - # And: the control still exists globally - control_resp = client.get(f"/api/v1/controls/{control_id}") - assert control_resp.status_code == 200 - assert control_resp.json()["id"] == control_id - - -def test_remove_policy_derived_control_reports_no_direct_association_removed( - client: TestClient, -) -> None: - # Given: an agent inheriting a control from a policy (no direct association) - payload = make_agent_payload() - init_resp = client.post("/api/v1/agents/initAgent", json=payload) - assert init_resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] - - policy_name = f"policy-{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"] - - control_name = f"control-{uuid.uuid4()}" - control_resp = client.put("/api/v1/controls", json={"name": control_name}) - assert control_resp.status_code == 200 - control_id = control_resp.json()["control_id"] - - from .utils import VALID_CONTROL_PAYLOAD - - set_data_resp = client.put( - f"/api/v1/controls/{control_id}/data", - json={"data": VALID_CONTROL_PAYLOAD}, - ) - assert set_data_resp.status_code == 200 - - assoc_control_resp = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - assert assoc_control_resp.status_code == 200 - assoc_policy_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") - assert assoc_policy_resp.status_code == 200 - - # When: removing via direct-control endpoint - remove_resp = client.delete(f"/api/v1/agents/{agent_id}/controls/{control_id}") - - # Then: request succeeds but reports that nothing direct was removed - assert remove_resp.status_code == 200 - assert remove_resp.json()["success"] is True - assert remove_resp.json()["removed_direct_association"] is False - assert remove_resp.json()["control_still_active"] is True - - # And: control remains active due to policy inheritance - list_resp = client.get(f"/api/v1/agents/{agent_id}/controls") - assert list_resp.status_code == 200 - assert {item["id"] for item in list_resp.json()["controls"]} == {control_id} - - def test_list_agent_controls_agent_not_found_404(client: TestClient) -> None: # Given: random agent id missing = str(uuid.uuid4()) @@ -521,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", }, @@ -561,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": {}}, @@ -587,59 +534,40 @@ 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_ids"] == [] + 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_ids"] == [] + assert agent2["policy_id"] is None def test_list_agents_with_policy(client: TestClient) -> None: - """Test that list agents shows policy_ids when assigned.""" + """Test that list agents shows policy_id when assigned.""" # Given: an agent with a policy assigned payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) agent_id = payload["agent"]["agent_id"] policy_id = _create_policy(client) - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # When: listing agents resp = client.get("/api/v1/agents") - # Then: the agent shows the policy_ids + # Then: the agent shows the policy_id assert resp.status_code == 200 body = resp.json() assert len(body["agents"]) == 1 - assert body["agents"][0]["policy_ids"] == [policy_id] - - -def test_list_agents_name_filter_treats_wildcards_as_literals(client: TestClient) -> None: - """Test name filter escapes SQL wildcard characters in user input.""" - literal_name = f"Agent_{uuid.uuid4().hex[:6]}" - wildcard_match_name = literal_name.replace("_", "X", 1) - - literal_payload = make_agent_payload(agent_id=str(uuid.uuid4()), name=literal_name) - wildcard_payload = make_agent_payload(agent_id=str(uuid.uuid4()), name=wildcard_match_name) - - assert client.post("/api/v1/agents/initAgent", json=literal_payload).status_code == 200 - assert client.post("/api/v1/agents/initAgent", json=wildcard_payload).status_code == 200 - - resp = client.get("/api/v1/agents", params={"name": literal_name}) - assert resp.status_code == 200 - names = [agent["agent_name"] for agent in resp.json()["agents"]] - - assert names == [literal_name] - assert resp.json()["pagination"]["total"] == 1 + assert body["agents"][0]["policy_id"] == policy_id def test_list_agents_pagination(client: TestClient) -> None: @@ -686,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 b68bdaa3..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( @@ -180,7 +181,7 @@ def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: Test policy_id, control_id, control_name = _create_policy_with_agent_evaluator_control( client, agent_name=agent_name, evaluator_name=evaluator_name ) - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign_resp.status_code == 200 # When: overwrite mode removes the evaluator from the incoming registration payload. @@ -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 9273aa03..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", }, @@ -298,7 +301,7 @@ def test_policy_assignment_with_builtin_evaluator(client: TestClient) -> None: ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: assert resp.status_code == 200 @@ -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, @@ -330,7 +333,7 @@ def test_policy_assignment_with_registered_agent_evaluator(client: TestClient) - ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Then: assert resp.status_code == 200 @@ -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) @@ -399,13 +402,13 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No ) # When: Assign to Agent A (should succeed) - resp_a = client.post(f"/api/v1/agents/{agent_a_id}/policies/{policy_id}") + resp_a = client.post(f"/api/v1/agents/{agent_a_id}/policy/{policy_id}") # Then: assert resp_a.status_code == 200 # When: Assign same policy to Agent B (should fail) - resp_b = client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_id}") + resp_b = client.post(f"/api/v1/agents/{agent_b_id}/policy/{policy_id}") # Then: (RFC 7807 format) assert resp_b.status_code == 400 @@ -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, @@ -561,7 +564,7 @@ def test_patch_agent_remove_evaluator_blocked_by_control(client: TestClient) -> ) # And: Policy assigned to agent - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert assign_resp.status_code == 200 # When: Trying to remove the evaluator @@ -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 e5567e82..01d02be6 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}", @@ -354,7 +342,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" @@ -363,11 +351,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 338bdb48..9643ca8f 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, @@ -219,12 +218,12 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() ) 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="steer", matched=True, @@ -232,7 +231,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() trace_id="a" * 32, ), _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=2, action="warn", matched=True, @@ -240,7 +239,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() trace_id="b" * 32, ), _event( - agent_uuid=agent_uuid, + agent_name=agent_name, control_id=3, action="allow", matched=True, @@ -254,7 +253,7 @@ async def test_postgres_event_store_timeseries_includes_steer_and_warn_counts() # When: querying stats with timeseries enabled stats = await store.query_stats( - agent_uuid, + agent_name, time_range=timedelta(hours=1), include_timeseries=True, bucket_size=timedelta(minutes=1), @@ -282,8 +281,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 136ac9f9..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: @@ -69,7 +69,7 @@ def test_agent_gets_controls_from_policy(client: TestClient) -> None: assert resp.status_code == 200 # Assign policy to agent - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") assert resp.status_code == 200 # When: Get agent's controls @@ -96,7 +96,7 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) client.post(f"/api/v1/policies/{policy_id}/controls/{control_1_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Verify initial state: 2 controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -123,8 +123,8 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) assert control_ids == {control_1_id, control_2_id, control_3_id, control_4_id, control_5_id} -def test_adding_second_agent_policy_unions_controls(client: TestClient) -> None: - """Adding another policy should union controls from both policies.""" +def test_switching_agent_policy_changes_controls(client: TestClient) -> None: + """Switching agent's policy should completely change its controls.""" # Given: Two policies with different controls agent_id, _ = _create_agent(client) @@ -143,26 +143,21 @@ def test_adding_second_agent_policy_unions_controls(client: TestClient) -> None: client.post(f"/api/v1/policies/{policy_b_id}/controls/{control_4_id}") # Assign policy A to agent - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_a_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_a_id}") resp = client.get(f"/api/v1/agents/{agent_id}/controls") controls_a = resp.json()["controls"] assert len(controls_a) == 2 assert {r["id"] for r in controls_a} == {control_1_id, control_2_id} - # When: Add policy B - resp = client.post(f"/api/v1/agents/{agent_id}/policies/{policy_b_id}") + # When: Switch to policy B + resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_b_id}") assert resp.status_code == 200 - # Then: Agent sees controls from both policies + # Then: Agent's controls change completely resp = client.get(f"/api/v1/agents/{agent_id}/controls") controls_b = resp.json()["controls"] - assert len(controls_b) == 4 - assert {r["id"] for r in controls_b} == { - control_1_id, - control_2_id, - control_3_id, - control_4_id, - } + assert len(controls_b) == 2 + assert {r["id"] for r in controls_b} == {control_3_id, control_4_id} def test_removing_agent_policy_clears_controls(client: TestClient) -> None: @@ -173,14 +168,14 @@ def test_removing_agent_policy_clears_controls(client: TestClient) -> None: control_id = _create_control(client, "control-1", {"id": 1}) client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Verify agent has controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") assert len(resp.json()["controls"]) > 0 # When: Remove policy from agent - resp = client.delete(f"/api/v1/agents/{agent_id}/policies") + resp = client.delete(f"/api/v1/agents/{agent_id}/policy") assert resp.status_code == 200 # Then: Agent returns empty controls list @@ -204,7 +199,7 @@ def test_removing_control_from_policy_removes_from_agent(client: TestClient) -> client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_3_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_4_id}") - client.post(f"/api/v1/agents/{agent_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") # Verify initial state: 4 controls resp = client.get(f"/api/v1/agents/{agent_id}/controls") @@ -238,8 +233,8 @@ def test_multiple_agents_same_policy(client: TestClient) -> None: client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") # Assign same policy to both agents - client.post(f"/api/v1/agents/{agent_1_id}/policies/{policy_id}") - client.post(f"/api/v1/agents/{agent_2_id}/policies/{policy_id}") + client.post(f"/api/v1/agents/{agent_1_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_2_id}/policy/{policy_id}") # Verify both see same controls initially resp_1 = client.get(f"/api/v1/agents/{agent_1_id}/controls") @@ -286,11 +281,11 @@ 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}/policies/{policy_a_id}") - client.post(f"/api/v1/agents/{agent_b_id}/policies/{policy_b_id}") + 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}") resp_a = client.get(f"/api/v1/agents/{agent_a_id}/controls") resp_b = client.get(f"/api/v1/agents/{agent_b_id}/controls") diff --git a/server/tests/test_services_controls.py b/server/tests/test_services_controls.py index b560fcb2..797835d2 100644 --- a/server/tests/test_services_controls.py +++ b/server/tests/test_services_controls.py @@ -7,14 +7,7 @@ from agent_control_models.errors import ErrorCode from agent_control_server.errors import APIValidationError -from agent_control_server.models import ( - Agent, - Control, - Policy, - agent_controls, - agent_policies, - policy_controls, -) +from agent_control_server.models import Agent, Control, Policy, policy_controls from agent_control_server.services.controls import list_controls_for_agent, list_controls_for_policy from .utils import VALID_CONTROL_PAYLOAD @@ -49,56 +42,44 @@ async def test_list_controls_for_policy_returns_controls(async_db) -> None: @pytest.mark.asyncio async def test_list_controls_for_agent_returns_controls(async_db) -> None: - # Given: an agent associated with one policy control and one direct control + # Given: an agent assigned to a policy with one control policy = Policy(name=f"policy-{uuid.uuid4()}") - policy_control = Control(name=f"policy-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) - direct_control = Control(name=f"direct-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) + 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, ) - async_db.add_all([policy, policy_control, direct_control, agent]) + async_db.add_all([policy, control, agent]) await async_db.flush() await async_db.execute( - insert(agent_policies).values({"agent_uuid": agent.agent_uuid, "policy_id": policy.id}) - ) - await async_db.execute( - insert(policy_controls).values({"policy_id": policy.id, "control_id": policy_control.id}) - ) - await async_db.execute( - insert(agent_controls).values( - {"agent_uuid": agent.agent_uuid, "control_id": direct_control.id} - ) + insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) ) 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) == 2 - names = {control.name for control in controls} - assert names == {policy_control.name, direct_control.name} + assert len(controls) == 1 + assert controls[0].name == control.name + assert controls[0].control.evaluator.name == VALID_CONTROL_PAYLOAD["evaluator"]["name"] @pytest.mark.asyncio async def test_list_controls_for_agent_corrupted_data_raises(async_db) -> None: - # Given: an agent associated with a policy containing corrupted control data + # Given: an agent assigned to a policy with corrupted control data 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, ) async_db.add_all([policy, control, agent]) await async_db.flush() - await async_db.execute( - insert(agent_policies).values({"agent_uuid": agent.agent_uuid, "policy_id": policy.id}) - ) await async_db.execute( insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) ) @@ -106,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 166a8c33..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)}/policies/{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 From ee9daa023c97914ebe32524a5d0b97b903c02f11 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 14:47:08 -0800 Subject: [PATCH 19/32] fix: drop stale agent_uuid references --- docs/observability.md | 40 +- docs/testing.md | 2 +- examples/agent_control_demo/setup_controls.py | 42 +- .../agent_control_demo/update_controls.py | 22 +- examples/crewai/setup_content_controls.py | 8 +- examples/customer_support_agent/demo.sh | 10 +- examples/customer_support_agent/run_demo.py | 10 +- .../setup_demo_controls.py | 11 +- examples/deepeval/setup_controls.py | 10 +- examples/langchain/setup_sql_controls.py | 10 +- examples/langchain/sql_agent_protection.py | 5 +- models/README.md | 14 +- sdks/python/README.md | 18 +- .../funcs/observability-get-control-stats.ts | 4 +- .../funcs/observability-get-stats.ts | 4 +- .../funcs/observability-query-events.ts | 2 +- .../models/control-execution-event.ts | 14 +- .../models/control-stats-response.ts | 10 +- .../generated/models/evaluation-request.ts | 12 +- .../generated/models/event-query-request.ts | 12 +- ...rvability-stats-controls-control-id-get.ts | 8 +- ...et-stats-api-v1-observability-stats-get.ts | 8 +- .../src/generated/models/stats-response.ts | 10 +- .../src/generated/sdk/observability.ts | 6 +- server/README.md | 67 +-- ui/src/core/api/client.ts | 50 +- ui/src/core/api/generated/api-types.ts | 564 ++++++++---------- .../hooks/query-hooks/use-agent-monitor.ts | 8 +- .../hooks/query-hooks/use-has-monitor-data.ts | 8 +- ui/tests/fixtures.ts | 33 +- 30 files changed, 471 insertions(+), 551 deletions(-) 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/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/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/demo.sh b/examples/customer_support_agent/demo.sh index 859c338a..5959589d 100755 --- a/examples/customer_support_agent/demo.sh +++ b/examples/customer_support_agent/demo.sh @@ -222,13 +222,13 @@ setup_demo_controls() { python setup_demo_controls.py } -# Agent UUID used by the demo (must match support_agent.py) -get_agent_uuid() { +# Agent name used by the demo (must match support_agent.py) +get_agent_name() { echo "646d5dea-c2e6-4453-b446-7035482b38e4" } show_observability_stats() { - local agent_uuid=$(get_agent_uuid) + local agent_name=$(get_agent_name) 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/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/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/sdks/python/README.md b/sdks/python/README.md index aa3379de..179306bc 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, @@ -172,7 +172,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) @@ -291,7 +291,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/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/control-execution-event.ts b/sdks/typescript/src/generated/models/control-execution-event.ts index 1f518a3b..e49b3005 100644 --- a/sdks/typescript/src/generated/models/control-execution-event.ts +++ b/sdks/typescript/src/generated/models/control-execution-event.ts @@ -70,8 +70,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) @@ -92,13 +91,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' */ @@ -198,7 +193,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(), @@ -218,7 +212,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", @@ -237,7 +230,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; @@ -263,7 +255,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(), @@ -283,7 +274,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 48684d59..e9727a51 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 8e88dd77..89c3de92 100644 --- a/sdks/typescript/src/generated/models/event-query-request.ts +++ b/sdks/typescript/src/generated/models/event-query-request.ts @@ -38,7 +38,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, steer, warn, log) * matched: Filter by matched status @@ -55,9 +55,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 */ @@ -121,7 +121,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; @@ -142,7 +142,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())), @@ -161,7 +161,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/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/stats-response.ts b/sdks/typescript/src/generated/models/stats-response.ts index e3be9657..eb6a68cc 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/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/server/README.md b/server/README.md index 09d0cba5..39435561 100644 --- a/server/README.md +++ b/server/README.md @@ -53,8 +53,10 @@ agent-control-server Create a `.env` file in the server directory: ```env -# Database -DB_URL=postgresql+psycopg://user:password@localhost/agent_control +# Database (use DATABASE_URL for Docker, DB_URL for local dev) +DATABASE_URL=postgresql+psycopg://user:password@localhost/agent_control +# Or use DB_URL (legacy): +# DB_URL=postgresql+psycopg://user:password@localhost/agent_control # Or for development: # DB_URL=sqlite+aiosqlite:///./agent_control.db @@ -155,45 +157,32 @@ GET /api/v1/evaluators ```bash # Register or update agent -POST /api/v1/agents/initAgent -Body: { "agent": {...}, "steps": [...], "evaluators": [...], "force_replace": false } +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 active controls for agent (union of policy-derived + direct agent controls) -GET /api/v1/agents/{agent_id}/controls - -# Add/remove policy associations on agent (many-to-many) -POST /api/v1/agents/{agent_id}/policies/{policy_id} -GET /api/v1/agents/{agent_id}/policies -DELETE /api/v1/agents/{agent_id}/policies/{policy_id} -DELETE /api/v1/agents/{agent_id}/policies - -# Add/remove direct control associations on agent (many-to-many) -POST /api/v1/agents/{agent_id}/controls/{control_id} -DELETE /api/v1/agents/{agent_id}/controls/{control_id} +# List controls for agent (based on assigned policy) +GET /api/v1/agents/{agent_name}/controls ``` ### Control Management ```bash # Create control -PUT /api/v1/controls -Body: { "name": "my-control" } +POST /api/v1/controls +Body: { "control": {...} } # List controls -GET /api/v1/controls?limit=100 +GET /api/v1/controls?skip=0&limit=100 # Get control GET /api/v1/controls/{control_id} -# Update control metadata -PATCH /api/v1/controls/{control_id} - -# Set control data -PUT /api/v1/controls/{control_id}/data -Body: { "data": {...} } +# Update control +PUT /api/v1/controls/{control_id} +Body: { "control": {...} } # Delete control DELETE /api/v1/controls/{control_id} @@ -203,17 +192,17 @@ DELETE /api/v1/controls/{control_id} ```bash # Create policy -PUT /api/v1/policies -Body: { "name": "my-policy" } +POST /api/v1/policies +Body: { "name": "my-policy", "description": "..." } -# Add control to policy -POST /api/v1/policies/{policy_id}/controls/{control_id} +# List policies +GET /api/v1/policies -# Remove control from policy -DELETE /api/v1/policies/{policy_id}/controls/{control_id} +# Assign policy to agent +POST /api/v1/policies/{policy_id}/agents/{agent_name} -# List policy controls -GET /api/v1/policies/{policy_id}/controls +# Add control to policy +POST /api/v1/policies/{policy_id}/controls/{control_id} ``` ### Evaluation @@ -222,7 +211,7 @@ GET /api/v1/policies/{policy_id}/controls # Evaluate step against controls POST /api/v1/evaluation Body: { - "agent_uuid": "uuid", + "agent_name": "uuid", "step": { "type": "llm", "name": "chat", "input": "..." }, "stage": "pre" } @@ -244,13 +233,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. @@ -321,7 +310,7 @@ docker build -f server/Dockerfile -t agent-control-server . # Run container docker run -p 8000:8000 \ - -e DB_URL=postgresql://... \ + -e DATABASE_URL=postgresql+psycopg://user:password@host:5432/agent_control \ -e AGENT_CONTROL_API_KEY_ENABLED=true \ -e AGENT_CONTROL_API_KEYS=your-key-here \ agent-control-server diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index 9213c4af..3facbcb0 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -55,42 +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 } }, }), - addPolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => - apiClient.POST('/api/v1/agents/{agent_id}/policies/{policy_id}', { - params: { path: { agent_id: agentId, policy_id: policyId } }, - }), - removePolicy: (agentId: GetAgentPathParams['agent_id'], policyId: number) => - apiClient.DELETE('/api/v1/agents/{agent_id}/policies/{policy_id}', { - params: { path: { agent_id: agentId, policy_id: policyId } }, - }), - getPolicies: (agentId: GetAgentPathParams['agent_id']) => - apiClient.GET('/api/v1/agents/{agent_id}/policies', { - params: { path: { agent_id: agentId } }, - }), - clearPolicies: (agentId: GetAgentPathParams['agent_id']) => - apiClient.DELETE('/api/v1/agents/{agent_id}/policies', { - params: { path: { agent_id: agentId } }, + 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 } }, }), - addControl: (agentId: GetAgentPathParams['agent_id'], controlId: number) => - apiClient.POST('/api/v1/agents/{agent_id}/controls/{control_id}', { - params: { path: { agent_id: agentId, control_id: controlId } }, + getPolicy: (agentName: GetAgentPathParams['agent_name']) => + apiClient.GET('/api/v1/agents/{agent_name}/policy', { + params: { path: { agent_name: agentName } }, }), - removeControl: ( - agentId: GetAgentPathParams['agent_id'], - controlId: number - ) => - apiClient.DELETE('/api/v1/agents/{agent_id}/controls/{control_id}', { - params: { path: { agent_id: agentId, control_id: controlId } }, + deletePolicy: (agentName: GetAgentPathParams['agent_name']) => + apiClient.DELETE('/api/v1/agents/{agent_name}/policy', { + params: { path: { agent_name: agentName } }, }), }, evaluators: { @@ -158,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 603aca36..339c1417 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 associations, + * 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,23 +51,18 @@ 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 UUID exists with a different name, returns 409 Conflict (no renames) + * - 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 * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update + * InitAgentResponse with created flag and active controls (if policy assigned) */ post: operations['init_agent_api_v1_agents_initAgent_post']; delete?: never; @@ -76,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; @@ -90,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: @@ -100,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; @@ -114,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) * @@ -125,10 +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']; + 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; @@ -139,19 +134,20 @@ export interface paths { * List agent's active controls * @description List all protection controls active for an agent. * - * Controls include the union of policy-derived and directly associated controls. + * Controls are inherited from the agent's assigned policy. + * 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: - * AgentControlsResponse with list of active controls + * AgentControlsResponse with list of controls (empty if no policy) * * 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; @@ -160,31 +156,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate control directly with agent - * @description Associate a control directly with an agent (idempotent). - */ - post: operations['add_agent_control_api_v1_agents__agent_id__controls__control_id__post']; - /** - * Remove direct control association from agent - * @description Remove a direct control association from an agent (idempotent). - */ - delete: operations['remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_id}/evaluators': { + '/api/v1/agents/{agent_name}/evaluators': { parameters: { query?: never; header?: never; @@ -200,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) @@ -211,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; @@ -220,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; @@ -232,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) * @@ -242,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; @@ -251,7 +223,7 @@ export interface paths { patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}/policies': { + '/api/v1/agents/{agent_name}/policy': { parameters: { query?: never; header?: never; @@ -259,23 +231,46 @@ export interface paths { cookie?: never; }; /** - * List policies associated with agent - * @description List policy IDs associated with an agent. + * Get agent's assigned policy + * @description Retrieve the policy currently assigned to an agent. + * + * Args: + * agent_name: Agent identifier + * 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_policies_api_v1_agents__agent_id__policies_get']; + get: operations['get_agent_policy_api_v1_agents__agent_name__policy_get']; put?: never; post?: never; /** - * Remove all policy associations from agent - * @description Remove all policy associations from an agent. + * 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_name: Agent identifier + * 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['remove_all_agent_policies_api_v1_agents__agent_id__policies_delete']; + delete: operations['delete_agent_policy_api_v1_agents__agent_name__policy_delete']; options?: never; head?: never; patch?: never; trace?: never; }; - '/api/v1/agents/{agent_id}/policies/{policy_id}': { + '/api/v1/agents/{agent_name}/policy/{policy_id}': { parameters: { query?: never; header?: never; @@ -285,15 +280,25 @@ export interface paths { get?: never; put?: never; /** - * Associate policy with agent - * @description Associate a policy with an agent (idempotent). - */ - post: operations['add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post']; - /** - * Remove policy association from agent - * @description Remove a policy association from an agent (idempotent). + * 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_name: Agent identifier + * 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 */ - delete: operations['remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete']; + post: operations['set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post']; + delete?: never; options?: never; head?: never; patch?: never; @@ -411,7 +416,7 @@ export interface paths { * Delete a control * @description Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy or agent. + * By default, deletion fails if the control is associated with any policy. * Use force=true to automatically dissociate and delete. * * Args: @@ -420,7 +425,7 @@ export interface paths { * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and dissociation details + * DeleteControlResponse with success flag and list of dissociated policies * * Raises: * HTTPException 404: Control not found @@ -594,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; @@ -651,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, steer, warn, log) * - matched: Filter by matched status @@ -690,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) @@ -722,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) @@ -913,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" @@ -936,12 +940,6 @@ export interface components { * @description Optional description of the agent's purpose */ agent_description?: string | null; - /** - * Agent Id - * Format: uuid - * @description Unique identifier for the agent (UUID format) - */ - agent_id: string; /** * Agent Metadata * @description Free-form metadata dictionary for custom properties @@ -951,7 +949,7 @@ export interface components { } | null; /** * Agent Name - * @description Human-readable name for the agent + * @description Unique immutable identifier for the agent */ agent_name: string; /** @@ -969,7 +967,7 @@ export interface components { AgentControlsResponse: { /** * Controls - * @description List of active controls associated with the agent + * @description List of controls associated with the agent via its policy */ controls: components['schemas']['Control'][]; }; @@ -978,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; }; @@ -996,18 +989,13 @@ export interface components { AgentSummary: { /** * Active Controls Count - * @description Number of active controls for this agent + * @description Number of active controls from agent's policy * @default 0 */ active_controls_count: number; - /** - * Agent Id - * @description UUID of the agent - */ - agent_id: string; /** * Agent Name - * @description Human-readable name of the agent + * @description Unique identifier of the agent */ agent_name: string; /** @@ -1022,10 +1010,10 @@ export interface components { */ evaluator_count: number; /** - * Policy Ids - * @description IDs of policies associated with the agent + * Policy Id + * @description ID of assigned policy, if any */ - policy_ids?: number[]; + policy_id?: number | null; /** * Step Count * @description Number of steps registered with the agent @@ -1055,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, @@ -1108,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. @@ -1239,8 +1235,7 @@ 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) * check_stage: "pre" (before execution) or "post" (after execution) @@ -1257,7 +1252,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, @@ -1282,15 +1276,9 @@ export interface components { action: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; /** * Agent Name - * @description Name of the agent (denormalized) + * @description Identifier of the agent */ agent_name: string; - /** - * Agent Uuid - * Format: uuid - * @description UUID of the agent - */ - agent_uuid: string; /** * Applies To * @description Type of call: 'llm_call' or 'tool_call' @@ -1577,7 +1565,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 @@ -1585,11 +1573,10 @@ export interface components { */ ControlStatsResponse: { /** - * Agent Uuid - * Format: uuid - * @description Agent UUID + * Agent Name + * @description Agent identifier */ - agent_uuid: string; + agent_name: string; /** * Control Id * @description Control ID @@ -1656,12 +1643,6 @@ export interface components { tags?: string[]; /** @description Agent using this control */ used_by_agent?: components['schemas']['AgentRef'] | null; - /** - * Used By Agents Count - * @description Number of unique agents using this control - * @default 0 - */ - used_by_agents_count: number; }; /** CreateControlRequest */ CreateControlRequest: { @@ -1729,15 +1710,10 @@ export interface components { */ DeleteControlResponse: { /** - * Dissociated From Agents - * @description Agent IDs the control was removed from before deletion - */ - dissociated_from_agents?: string[]; - /** - * Dissociated From Policies + * Dissociated From * @description Policy IDs the control was removed from before deletion */ - dissociated_from_policies?: number[]; + dissociated_from?: number[]; /** * Success * @description Whether the control was deleted @@ -1755,6 +1731,14 @@ export interface components { */ success: boolean; }; + /** DeletePolicyResponse */ + DeletePolicyResponse: { + /** + * Success + * @description Whether the policy was successfully removed + */ + success: boolean; + }; /** * EvaluationRequest * @description Request model for evaluation analysis. @@ -1763,11 +1747,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": { @@ -1780,7 +1764,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "post", * "step": { * "context": { @@ -1794,7 +1778,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "pre", * "step": { * "context": { @@ -1808,7 +1792,7 @@ export interface components { * } * } * @example { - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440000", + * "agent_name": "customer-service-bot", * "stage": "post", * "step": { * "context": { @@ -1827,11 +1811,10 @@ 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; + agent_name: string; /** * Stage * @description Evaluation stage: 'pre' or 'post' @@ -2098,7 +2081,7 @@ 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, steer, warn, log) * matched: Filter by matched status @@ -2116,7 +2099,7 @@ export interface components { * "deny", * "warn" * ], - * "agent_uuid": "550e8400-e29b-41d4-a716-446655440001", + * "agent_name": "my-agent", * "limit": 50, * "start_time": "2025-01-09T00:00:00Z" * } @@ -2128,10 +2111,10 @@ export interface components { */ actions?: ('allow' | 'deny' | 'steer' | 'warn' | 'log')[] | null; /** - * Agent Uuid - * @description Filter by agent UUID + * Agent Name + * @description Filter by agent identifier */ - agent_uuid?: string | null; + agent_name?: string | null; /** * Applies To * @description Filter by call types @@ -2222,14 +2205,6 @@ export interface components { */ total: number; }; - /** GetAgentPoliciesResponse */ - GetAgentPoliciesResponse: { - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - }; /** * GetAgentResponse * @description Response containing agent details and registered steps. @@ -2282,6 +2257,14 @@ export interface components { */ control_ids: number[]; }; + /** GetPolicyResponse */ + GetPolicyResponse: { + /** + * Policy Id + * @description Identifier of the policy assigned to the agent + */ + policy_id: number; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -2301,13 +2284,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" * }, @@ -2346,6 +2402,11 @@ export interface components { InitAgentRequest: { /** @description Agent metadata including ID, name, and version */ agent: components['schemas']['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 + */ + conflict_mode: components['schemas']['ConflictMode']; /** * Evaluators * @description Custom evaluator schemas for config validation @@ -2370,7 +2431,7 @@ export interface components { InitAgentResponse: { /** * Controls - * @description Active protection controls for the agent + * @description Active protection controls for the agent (if policy assigned) */ controls?: components['schemas']['Control'][]; /** @@ -2378,6 +2439,14 @@ export interface components { * @description True if agent was newly created, False if updated */ created: boolean; + /** + * Overwrite Applied + * @description True if overwrite mode changed registration data on an existing agent + * @default false + */ + overwrite_applied: boolean; + /** @description Detailed list of changes applied in overwrite mode */ + overwrite_changes?: components['schemas']['InitAgentOverwriteChanges']; }; JSONObject: { [key: string]: components['schemas']['JSONValue']; @@ -2527,27 +2596,6 @@ export interface components { */ success: boolean; }; - /** - * RemoveAgentControlResponse - * @description Response for removing a direct agent-control association. - */ - RemoveAgentControlResponse: { - /** - * Control Still Active - * @description True if the control remains active via policy association(s) - */ - control_still_active: boolean; - /** - * Removed Direct Association - * @description True if a direct agent-control link was removed - */ - removed_direct_association: boolean; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; /** * SetControlDataRequest * @description Request to update control configuration data. @@ -2564,6 +2612,19 @@ export interface components { */ success: boolean; }; + /** SetPolicyResponse */ + SetPolicyResponse: { + /** + * 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 * @description Response model for agent-level aggregated statistics. @@ -2571,18 +2632,17 @@ 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_uuid: string; + agent_name: string; /** * Controls * @description Per-control breakdown @@ -2960,12 +3020,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; }; @@ -2991,12 +3051,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; }; @@ -3026,18 +3086,18 @@ export interface operations { }; }; }; - 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; }; requestBody?: never; responses: { - /** @description List of controls from agent policy and direct associations */ + /** @description List of controls from agent's policy */ 200: { headers: { [name: string]: unknown; @@ -3057,71 +3117,7 @@ export interface operations { }; }; }; - add_agent_control_api_v1_agents__agent_id__controls__control_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - 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_agent_control_api_v1_agents__agent_id__controls__control_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['RemoveAgentControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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; @@ -3129,7 +3125,7 @@ export interface operations { }; header?: never; path: { - agent_id: string; + agent_name: string; }; cookie?: never; }; @@ -3155,12 +3151,12 @@ export interface operations { }; }; }; - get_agent_evaluator_api_v1_agents__agent_id__evaluators__evaluator_name__get: { + get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { parameters: { query?: never; header?: never; path: { - agent_id: string; + agent_name: string; evaluator_name: string; }; cookie?: never; @@ -3187,55 +3183,24 @@ export interface operations { }; }; }; - get_agent_policies_api_v1_agents__agent_id__policies_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of policy IDs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAgentPoliciesResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - remove_all_agent_policies_api_v1_agents__agent_id__policies_delete: { + get_agent_policy_api_v1_agents__agent_name__policy_get: { parameters: { query?: never; header?: never; path: { - agent_id: string; + 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 */ @@ -3249,13 +3214,12 @@ export interface operations { }; }; }; - add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post: { + delete_agent_policy_api_v1_agents__agent_name__policy_delete: { parameters: { query?: never; header?: never; path: { - agent_id: string; - policy_id: number; + agent_name: string; }; cookie?: never; }; @@ -3267,7 +3231,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AssocResponse']; + 'application/json': components['schemas']['DeletePolicyResponse']; }; }; /** @description Validation Error */ @@ -3281,25 +3245,25 @@ export interface operations { }; }; }; - remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete: { + set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post: { parameters: { query?: never; header?: never; path: { - agent_id: string; + agent_name: string; policy_id: number; }; cookie?: never; }; requestBody?: never; responses: { - /** @description Success confirmation */ + /** @description Success status with previous policy ID */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['AssocResponse']; + 'application/json': components['schemas']['SetPolicyResponse']; }; }; /** @description Validation Error */ @@ -3458,7 +3422,7 @@ export interface operations { delete_control_api_v1_controls__control_id__delete: { parameters: { query?: { - /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + /** @description If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. */ force?: boolean; }; header?: never; @@ -3884,7 +3848,7 @@ export interface operations { get_stats_api_v1_observability_stats_get: { parameters: { query: { - agent_uuid: string; + agent_name: string; time_range?: | '1m' | '5m' @@ -3926,7 +3890,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' 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-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/tests/fixtures.ts b/ui/tests/fixtures.ts index 42c2fcdc..1eb14a23 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -20,27 +20,24 @@ 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', - policy_ids: [1], + agent_name: 'customer-support-bot', + policy_id: 1, created_at: '2024-01-01T00:00:00Z', step_count: 5, evaluator_count: 2, active_controls_count: 3, }, { - agent_id: 'agent-2', - agent_name: 'Data Analysis Agent', - policy_ids: [2], + agent_name: 'data-analysis-agent', + policy_id: 2, created_at: '2024-01-02T00:00:00Z', step_count: 3, evaluator_count: 1, active_controls_count: 2, }, { - agent_id: 'agent-3', - agent_name: 'Code Review Assistant', - policy_ids: [3], + agent_name: 'code-review-assistant', + policy_id: 3, created_at: '2024-01-03T00:00:00Z', step_count: 8, evaluator_count: 4, @@ -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', @@ -147,7 +143,9 @@ const controlsResponse: AgentControlsResponse = { }; // Control summaries for GET /api/v1/controls (list all controls) -const controlSummariesList: ControlSummary[] = [ +const controlSummariesList: (ControlSummary & { + used_by_agent?: { agent_name: string } | null; +})[] = [ { id: 1, name: 'PII Detection', @@ -157,8 +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_agents_count: 1, + used_by_agent: { agent_name: 'customer-support-bot' }, }, { id: 2, @@ -169,8 +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_agents_count: 1, + used_by_agent: { agent_name: 'data-analysis-agent' }, }, { id: 3, @@ -182,7 +178,6 @@ const controlSummariesList: ControlSummary[] = [ stages: ['pre'], tags: [], used_by_agent: null, - used_by_agents_count: 0, }, ]; @@ -270,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, @@ -335,7 +330,7 @@ const statsResponse: StatsResponse = { }; const emptyStatsResponse: StatsResponse = { - agent_uuid: 'agent-1', + agent_name: 'customer-support-bot', time_range: '1h', totals: { execution_count: 0, From f8c23a525fdecaf790db69f721f5cdec109deaea Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 14:55:52 -0800 Subject: [PATCH 20/32] chore: eliminate remaining agent_id references --- CONTRIBUTING.md | 2 +- README.md | 6 +- docs/REFERENCE.md | 14 +- examples/README.md | 4 +- examples/agent_control_demo/demo_agent.py | 2 +- examples/crewai/content_agent_protection.py | 2 +- examples/customer_support_agent/README.md | 2 +- .../customer_support_agent/support_agent.py | 2 +- examples/deepeval/qa_agent.py | 2 +- .../langchain/langgraph_auto_schema_agent.py | 2 +- sdks/python/ARCHITECTURE.md | 8 +- sdks/python/README.md | 2 +- sdks/python/tests/README.md | 2 +- sdks/python/tests/conftest.py | 10 +- sdks/python/tests/test_agents_api.py | 6 +- sdks/python/tests/test_integration_agents.py | 8 +- .../overlays/method-names.overlay.yaml | 37 +--- .../src/generated/funcs/agents-add-control.ts | 191 ----------------- ...nt-policies.ts => agents-delete-policy.ts} | 42 ++-- .../generated/funcs/agents-get-evaluator.ts | 14 +- ...s-get-policies.ts => agents-get-policy.ts} | 36 ++-- .../src/generated/funcs/agents-get.ts | 14 +- .../src/generated/funcs/agents-init.ts | 10 +- .../generated/funcs/agents-list-controls.ts | 19 +- .../generated/funcs/agents-list-evaluators.ts | 16 +- .../src/generated/funcs/agents-list.ts | 4 +- .../generated/funcs/agents-remove-control.ts | 191 ----------------- .../generated/funcs/agents-remove-policy.ts | 194 ------------------ ...-add-policy.ts => agents-update-policy.ts} | 40 ++-- .../src/generated/funcs/agents-update.ts | 14 +- .../src/generated/funcs/controls-delete.ts | 4 +- .../src/generated/funcs/evaluators-list.ts | 2 +- .../models/agent-controls-response.ts | 2 +- .../src/generated/models/agent-ref.ts | 8 +- .../src/generated/models/agent-summary.ts | 18 +- sdks/typescript/src/generated/models/agent.ts | 13 +- .../src/generated/models/control-summary.ts | 6 - .../models/delete-control-response.ts | 12 +- .../models/delete-policy-response.ts | 34 +++ .../generated/models/get-agent-response.ts | 2 +- ...ies-response.ts => get-policy-response.ts} | 22 +- sdks/typescript/src/generated/models/index.ts | 5 +- .../generated/models/init-agent-request.ts | 2 +- .../generated/models/init-agent-response.ts | 2 +- ...gents-agent-id-controls-control-id-post.ts | 46 ----- ...agents-agent-id-policies-policy-id-post.ts | 46 ----- ...-api-v1-agents-agent-name-policy-delete.ts | 42 ++++ ...ntrol-api-v1-controls-control-id-delete.ts | 2 +- .../get-agent-api-v1-agents-agent-id-get.ts | 40 ---- .../get-agent-api-v1-agents-agent-name-get.ts | 42 ++++ ...-agent-id-evaluators-evaluator-name-get.ts | 49 ----- ...gent-name-evaluators-evaluator-name-get.ts | 49 +++++ ...ies-api-v1-agents-agent-id-policies-get.ts | 42 ---- ...icy-api-v1-agents-agent-name-policy-get.ts | 42 ++++ .../src/generated/models/operations/index.ts | 19 +- ...ols-api-v1-agents-agent-id-controls-get.ts | 42 ---- ...s-api-v1-agents-agent-name-controls-get.ts | 41 ++++ ...s-api-v1-agents-agent-id-evaluators-get.ts | 48 ----- ...api-v1-agents-agent-name-evaluators-get.ts | 48 +++++ ...atch-agent-api-v1-agents-agent-id-patch.ts | 46 ----- ...ch-agent-api-v1-agents-agent-name-patch.ts | 46 +++++ ...nts-agent-id-controls-control-id-delete.ts | 49 ----- ...ents-agent-id-policies-policy-id-delete.ts | 46 ----- ...-api-v1-agents-agent-id-policies-delete.ts | 42 ---- ...agents-agent-name-policy-policy-id-post.ts | 46 +++++ .../models/remove-agent-control-response.ts | 56 ----- .../generated/models/set-policy-response.ts | 47 +++++ sdks/typescript/src/generated/sdk/agents.ts | 174 +++++++--------- sdks/typescript/src/generated/sdk/controls.ts | 4 +- .../src/generated/sdk/evaluators.ts | 2 +- .../test_generate_method_names_overlay.py | 6 +- server/tests/test_agents_additional.py | 147 +++++++------ server/tests/test_controls_additional.py | 14 +- server/tests/test_error_handling.py | 34 +-- server/tests/test_evaluator_schemas.py | 50 ++--- server/tests/test_init_agent.py | 78 +++---- server/tests/test_init_agent_conflict_mode.py | 16 +- server/tests/test_init_agent_force_replace.py | 24 +-- server/tests/test_new_features.py | 100 ++++----- server/tests/test_policy_integration.py | 42 ++-- ui/src/core/api/types.ts | 6 +- .../hooks/query-hooks/use-agent-controls.ts | 2 +- ui/src/core/hooks/query-hooks/use-agent.ts | 2 +- ui/src/core/layouts/app-layout.tsx | 6 +- .../agent-detail/agent-detail.tsx | 4 +- ui/src/core/page-components/home/home.tsx | 4 +- ui/tests/home.spec.ts | 2 +- 87 files changed, 984 insertions(+), 1737 deletions(-) delete mode 100644 sdks/typescript/src/generated/funcs/agents-add-control.ts rename sdks/typescript/src/generated/funcs/{agents-remove-all-agent-policies.ts => agents-delete-policy.ts} (80%) rename sdks/typescript/src/generated/funcs/{agents-get-policies.ts => agents-get-policy.ts} (82%) delete mode 100644 sdks/typescript/src/generated/funcs/agents-remove-control.ts delete mode 100644 sdks/typescript/src/generated/funcs/agents-remove-policy.ts rename sdks/typescript/src/generated/funcs/{agents-add-policy.ts => agents-update-policy.ts} (81%) create mode 100644 sdks/typescript/src/generated/models/delete-policy-response.ts rename sdks/typescript/src/generated/models/{get-agent-policies-response.ts => get-policy-response.ts} (51%) delete mode 100644 sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts delete mode 100644 sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts create mode 100644 sdks/typescript/src/generated/models/operations/delete-agent-policy-api-v1-agents-agent-name-policy-delete.ts delete mode 100644 sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-id-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/get-agent-api-v1-agents-agent-name-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-id-evaluators-evaluator-name-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/get-agent-evaluator-api-v1-agents-agent-name-evaluators-evaluator-name-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/get-agent-policy-api-v1-agents-agent-name-policy-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-id-controls-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/list-agent-controls-api-v1-agents-agent-name-controls-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-id-evaluators-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/list-agent-evaluators-api-v1-agents-agent-name-evaluators-get.ts delete mode 100644 sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-id-patch.ts create mode 100644 sdks/typescript/src/generated/models/operations/patch-agent-api-v1-agents-agent-name-patch.ts delete mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts delete mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts delete mode 100644 sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/set-agent-policy-api-v1-agents-agent-name-policy-policy-id-post.ts delete mode 100644 sdks/typescript/src/generated/models/remove-agent-control-response.ts create mode 100644 sdks/typescript/src/generated/models/set-policy-response.ts 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 08c70fd9..bd6a6c5f 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,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() ) @@ -202,7 +202,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"] ) @@ -236,7 +236,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 b65f6172..2d89706c 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 policy_refresh_interval_seconds=60, # Optional: set 0 to disable background refresh steps=[ # Optional: register available steps @@ -601,7 +601,7 @@ agent_control.init( ) ``` -When enabled, background refresh fetches controls via `GET /agents/{agent_id}/controls`. +When enabled, background refresh fetches controls via `GET /agents/{agent_name}/controls`. Refresh failures are fail-open: the SDK keeps the last successful local cache snapshot. ### The @control Decorator @@ -795,11 +795,11 @@ 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 | -| `POST` | `/agents/{agent_id}/controls/{control_id}` | Add control to agent | -| `DELETE` | `/agents/{agent_id}/controls/{control_id}` | Remove control from agent | +| `GET` | `/agents/{agent_name}` | Get agent details | +| `PATCH` | `/agents/{agent_name}` | Update agent | +| `GET` | `/agents/{agent_name}/controls` | List controls for agent | +| `POST` | `/agents/{agent_name}/controls/{control_id}` | Add control to agent | +| `DELETE` | `/agents/{agent_name}/controls/{control_id}` | Remove control from agent | **Controls**: diff --git a/examples/README.md b/examples/README.md index c6fd1d2a..e5c2dd9c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -129,7 +129,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 @@ -150,7 +150,7 @@ except ControlViolationError as e: import agent_control from agent_control import control, ControlSteerError, ControlViolationError -agent_control.init(agent_name="my-bot", agent_id="...") +agent_control.init(agent_name="my-bot", agent_name="...") @control() async def generate_content(prompt: str) -> str: diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 8a1e7cfe..4e033bb0 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -162,7 +162,7 @@ async def run_demo(): 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/crewai/content_agent_protection.py b/examples/crewai/content_agent_protection.py index 118be45a..ecef00f1 100644 --- a/examples/crewai/content_agent_protection.py +++ b/examples/crewai/content_agent_protection.py @@ -54,7 +54,7 @@ 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/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/support_agent.py b/examples/customer_support_agent/support_agent.py index 0cd7a876..821890d9 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -30,7 +30,7 @@ agent_control.init( agent_name="Customer Support Agent", - agent_id="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..90cfd4a8 100755 --- a/examples/deepeval/qa_agent.py +++ b/examples/deepeval/qa_agent.py @@ -38,7 +38,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/langchain/langgraph_auto_schema_agent.py b/examples/langchain/langgraph_auto_schema_agent.py index 7a6e1c3e..ac9acd11 100644 --- a/examples/langchain/langgraph_auto_schema_agent.py +++ b/examples/langchain/langgraph_auto_schema_agent.py @@ -242,7 +242,7 @@ async def main() -> None: 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/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 179306bc..e7811d4d 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -184,7 +184,7 @@ Initialize Agent Control with your agent's information. **Returns:** `Agent` instance When background refresh is enabled, the SDK refreshes cache snapshots via -`GET /agents/{agent_id}/controls`. On refresh failures, it keeps the previous +`GET /agents/{agent_name}/controls`. On refresh failures, it keeps the previous snapshot (fail-open behavior). ### Decorator diff --git a/sdks/python/tests/README.md b/sdks/python/tests/README.md index b4677039..b1dada52 100644 --- a/sdks/python/tests/README.md +++ b/sdks/python/tests/README.md @@ -152,7 +152,7 @@ PASSED ### Function-Scoped Fixtures - `client`: Authenticated AgentProtectClient instance - `unique_name`: Unique name generator for test resources -- `test_agent_id`: Unique agent UUID for testing +- `test_agent_name`: Unique agent UUID for testing - `test_agent`: Registered test agent - `test_policy`: Created test policy - `test_control`: Created test control diff --git a/sdks/python/tests/conftest.py b/sdks/python/tests/conftest.py index 6860c8e9..eabd7ec9 100644 --- a/sdks/python/tests/conftest.py +++ b/sdks/python/tests/conftest.py @@ -100,7 +100,7 @@ def unique_name() -> str: @pytest.fixture -def test_agent_id() -> str: +def test_agent_name() -> str: """Generate a unique agent name for testing.""" return f"agent-{uuid.uuid4().hex[:12]}" @@ -108,7 +108,7 @@ def test_agent_id() -> str: @pytest_asyncio.fixture async def test_agent( client: agent_control.AgentControlClient, - test_agent_id: str, + test_agent_name: str, server_url: str ) -> AsyncGenerator[dict[str, Any], None]: """ @@ -122,7 +122,7 @@ async def test_agent( from agent_control_models import Agent agent = Agent( - agent_name=test_agent_id, + agent_name=test_agent_name, agent_description="Integration test agent", agent_created_at=datetime.now(UTC).isoformat(), agent_updated_at=None, @@ -146,8 +146,8 @@ async def test_agent( yield { "agent": agent, - "agent_name": test_agent_id, - "agent_name": test_agent_id, + "agent_name": test_agent_name, + "agent_name": test_agent_name, "response": response } diff --git a/sdks/python/tests/test_agents_api.py b/sdks/python/tests/test_agents_api.py index 271271e0..01e59b36 100644 --- a/sdks/python/tests/test_agents_api.py +++ b/sdks/python/tests/test_agents_api.py @@ -20,13 +20,13 @@ async def test_list_agent_controls_typed_calls_controls_endpoint() -> None: response.raise_for_status = Mock() response.json = Mock(return_value=response_payload) client = SimpleNamespace(http_client=SimpleNamespace(get=AsyncMock(return_value=response))) - agent_id = str(uuid4()) + agent_name = str(uuid4()) # WHEN: typed controls are requested. - result = await agent_control.agents.list_agent_controls_typed(client, agent_id) + result = await agent_control.agents.list_agent_controls_typed(client, agent_name) # THEN: wrapper calls the expected endpoint and returns a typed result. - client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_id}/controls") + client.http_client.get.assert_awaited_once_with(f"/api/v1/agents/{agent_name}/controls") assert isinstance(result.controls, list) diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index 144081d5..ee8a9017 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -18,7 +18,7 @@ @pytest.mark.asyncio async def test_agent_registration_workflow( client: agent_control.AgentControlClient, - test_agent_id: str, + test_agent_name: str, sample_steps: list ) -> None: """ @@ -199,7 +199,7 @@ async def test_convenience_get_agent_function( @pytest.mark.asyncio async def test_init_function_workflow( - test_agent_id: str, + test_agent_name: str, server_url: str, api_key: str | None, sample_steps: list, @@ -214,7 +214,7 @@ async def test_init_function_workflow( """ # Initialize agent agent = agent_control.init( - agent_name=test_agent_id, + agent_name=test_agent_name, agent_description="Testing init function", agent_version="1.0.0", server_url=server_url, @@ -226,7 +226,7 @@ async def test_init_function_workflow( # Verify agent instance assert agent is not None - assert agent.agent_name == test_agent_id + assert agent.agent_name == test_agent_name assert hasattr(agent, "agent_name") # Verify current_agent() diff --git a/sdks/typescript/overlays/method-names.overlay.yaml b/sdks/typescript/overlays/method-names.overlay.yaml index 0907d56f..fd4927f7 100644 --- a/sdks/typescript/overlays/method-names.overlay.yaml +++ b/sdks/typescript/overlays/method-names.overlay.yaml @@ -15,60 +15,45 @@ 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}/controls/{control_id}"]["post"] - update: - x-speakeasy-group: agents - x-speakeasy-name-override: addControl - - - target: $["paths"]["/api/v1/agents/{agent_id}/controls/{control_id}"]["delete"] - update: - x-speakeasy-group: agents - x-speakeasy-name-override: removeControl - - - 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}/policies"]["get"] - update: - x-speakeasy-group: agents - x-speakeasy-name-override: getPolicies - - - target: $["paths"]["/api/v1/agents/{agent_id}/policies"]["delete"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy"]["get"] update: x-speakeasy-group: agents - x-speakeasy-name-override: removeAllAgentPolicies + x-speakeasy-name-override: getPolicy - - target: $["paths"]["/api/v1/agents/{agent_id}/policies/{policy_id}"]["post"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy"]["delete"] update: x-speakeasy-group: agents - x-speakeasy-name-override: addPolicy + x-speakeasy-name-override: deletePolicy - - target: $["paths"]["/api/v1/agents/{agent_id}/policies/{policy_id}"]["delete"] + - target: $["paths"]["/api/v1/agents/{agent_name}/policy/{policy_id}"]["post"] update: x-speakeasy-group: agents - x-speakeasy-name-override: removePolicy + x-speakeasy-name-override: updatePolicy - target: $["paths"]["/api/v1/controls"]["get"] update: diff --git a/sdks/typescript/src/generated/funcs/agents-add-control.ts b/sdks/typescript/src/generated/funcs/agents-add-control.ts deleted file mode 100644 index 8f889f87..00000000 --- a/sdks/typescript/src/generated/funcs/agents-add-control.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { AgentControlSDKCore } from "../core.js"; -import { encodeSimple } from "../lib/encodings.js"; -import * as M from "../lib/matchers.js"; -import { compactMap } from "../lib/primitives.js"; -import { safeParse } from "../lib/schemas.js"; -import { RequestOptions } from "../lib/sdks.js"; -import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; -import { pathToFunc } from "../lib/url.js"; -import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; -import { - ConnectionError, - InvalidRequestError, - RequestAbortedError, - RequestTimeoutError, - UnexpectedClientError, -} from "../models/errors/http-client-errors.js"; -import * as errors from "../models/errors/index.js"; -import { ResponseValidationError } from "../models/errors/response-validation-error.js"; -import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; -import * as models from "../models/index.js"; -import * as operations from "../models/operations/index.js"; -import { APICall, APIPromise } from "../types/async.js"; -import { Result } from "../types/fp.js"; - -/** - * Associate control directly with agent - * - * @remarks - * Associate a control directly with an agent (idempotent). - */ -export function agentsAddControl( - client: AgentControlSDKCore, - request: - operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, - options?: RequestOptions, -): APIPromise< - Result< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - > -> { - return new APIPromise($do( - client, - request, - options, - )); -} - -async function $do( - client: AgentControlSDKCore, - request: - operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, - options?: RequestOptions, -): Promise< - [ - Result< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >, - APICall, - ] -> { - const parsed = safeParse( - request, - (value) => - z.parse( - operations - .AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema, - value, - ), - "Input validation failed", - ); - if (!parsed.ok) { - return [parsed, { status: "invalid" }]; - } - const payload = parsed.value; - const body = null; - - const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { - explode: false, - charEncoding: "percent", - }), - control_id: encodeSimple("control_id", payload.control_id, { - explode: false, - charEncoding: "percent", - }), - }; - - const path = pathToFunc("/api/v1/agents/{agent_id}/controls/{control_id}")( - pathParams, - ); - - const headers = new Headers(compactMap({ - Accept: "application/json", - })); - - const secConfig = await extractSecurity(client._options.apiKeyHeader); - const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; - const requestSecurity = resolveGlobalSecurity(securityInput); - - const context = { - options: client._options, - baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: - "add_agent_control_api_v1_agents__agent_id__controls__control_id__post", - oAuth2Scopes: null, - - resolvedSecurity: requestSecurity, - - securitySource: client._options.apiKeyHeader, - retryConfig: options?.retries - || client._options.retryConfig - || { strategy: "none" }, - retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], - }; - - const requestRes = client._createRequest(context, { - security: requestSecurity, - method: "POST", - baseURL: options?.serverURL, - path: path, - headers: headers, - body: body, - userAgent: client._options.userAgent, - timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, - }, options); - if (!requestRes.ok) { - return [requestRes, { status: "invalid" }]; - } - const req = requestRes.value; - - const doResult = await client._do(req, { - context, - errorCodes: ["422", "4XX", "5XX"], - retryConfig: context.retryConfig, - retryCodes: context.retryCodes, - }); - if (!doResult.ok) { - return [doResult, { status: "request-error", request: req }]; - } - const response = doResult.value; - - const responseFields = { - HttpMeta: { Response: response, Request: req }, - }; - - const [result] = await M.match< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >( - M.json(200, models.AssocResponse$inboundSchema), - M.jsonErr(422, errors.HTTPValidationError$inboundSchema), - M.fail("4XX"), - M.fail("5XX"), - )(response, req, { extraFields: responseFields }); - if (!result.ok) { - return [result, { status: "complete", request: req, response }]; - } - - return [result, { status: "complete", request: req, response }]; -} diff --git a/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts similarity index 80% rename from sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts rename to sdks/typescript/src/generated/funcs/agents-delete-policy.ts index 165f4bad..2e8a18b6 100644 --- a/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts +++ b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts @@ -28,19 +28,31 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Remove all policy associations from agent + * Remove agent's policy assignment * * @remarks - * Remove all policy associations from an agent. + * Remove the policy assignment from an agent. + * + * The agent will no longer have any protection controls active. + * + * Args: + * agent_name: Agent identifier + * 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 */ -export function agentsRemoveAllAgentPolicies( +export function agentsDeletePolicy( client: AgentControlSDKCore, - request: - operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, + request: operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, ): APIPromise< Result< - models.AssocResponse, + models.DeletePolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -61,13 +73,12 @@ export function agentsRemoveAllAgentPolicies( async function $do( client: AgentControlSDKCore, - request: - operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, + request: operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, ): Promise< [ Result< - models.AssocResponse, + models.DeletePolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -86,7 +97,7 @@ async function $do( (value) => z.parse( operations - .RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema, + .DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest$outboundSchema, value, ), "Input validation failed", @@ -98,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}/policies")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/policy")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -117,8 +128,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: - "remove_all_agent_policies_api_v1_agents__agent_id__policies_delete", + operationID: "delete_agent_policy_api_v1_agents__agent_name__policy_delete", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -161,7 +171,7 @@ async function $do( }; const [result] = await M.match< - models.AssocResponse, + models.DeletePolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -172,7 +182,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.AssocResponse$inboundSchema), + M.json(200, models.DeletePolicyResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), 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-policies.ts b/sdks/typescript/src/generated/funcs/agents-get-policy.ts similarity index 82% rename from sdks/typescript/src/generated/funcs/agents-get-policies.ts rename to sdks/typescript/src/generated/funcs/agents-get-policy.ts index 6778b46e..cbe953f3 100644 --- a/sdks/typescript/src/generated/funcs/agents-get-policies.ts +++ b/sdks/typescript/src/generated/funcs/agents-get-policy.ts @@ -28,18 +28,28 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * List policies associated with agent + * Get agent's assigned policy * * @remarks - * List policy IDs associated with an agent. + * Retrieve the policy currently assigned to an agent. + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * GetPolicyResponse with policy ID + * + * Raises: + * HTTPException 404: Agent not found or agent has no policy assigned */ -export function agentsGetPolicies( +export function agentsGetPolicy( client: AgentControlSDKCore, - request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, ): APIPromise< Result< - models.GetAgentPoliciesResponse, + models.GetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -60,12 +70,12 @@ export function agentsGetPolicies( async function $do( client: AgentControlSDKCore, - request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, ): Promise< [ Result< - models.GetAgentPoliciesResponse, + models.GetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -84,7 +94,7 @@ async function $do( (value) => z.parse( operations - .GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema, + .GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest$outboundSchema, value, ), "Input validation failed", @@ -96,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}/policies")(pathParams); + const path = pathToFunc("/api/v1/agents/{agent_name}/policy")(pathParams); const headers = new Headers(compactMap({ Accept: "application/json", @@ -115,7 +125,7 @@ async function $do( const context = { options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: "get_agent_policies_api_v1_agents__agent_id__policies_get", + operationID: "get_agent_policy_api_v1_agents__agent_name__policy_get", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -158,7 +168,7 @@ async function $do( }; const [result] = await M.match< - models.GetAgentPoliciesResponse, + models.GetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -169,7 +179,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.GetAgentPoliciesResponse$inboundSchema), + M.json(200, models.GetPolicyResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), 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 9379bc20..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 @@ -47,11 +45,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update + * InitAgentResponse with created flag and active controls (if policy assigned) */ 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 8d517b8c..b44c3402 100644 --- a/sdks/typescript/src/generated/funcs/agents-list-controls.ts +++ b/sdks/typescript/src/generated/funcs/agents-list-controls.ts @@ -33,21 +33,22 @@ import { Result } from "../types/fp.js"; * @remarks * List all protection controls active for an agent. * - * Controls include the union of policy-derived and directly associated controls. + * Controls are inherited from the agent's assigned policy. + * 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: - * AgentControlsResponse with list of active controls + * AgentControlsResponse with list of controls (empty if no policy) * * Raises: * HTTPException 404: Agent not found */ export function agentsListControls( client: AgentControlSDKCore, - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): APIPromise< Result< @@ -72,7 +73,7 @@ export function agentsListControls( async function $do( client: AgentControlSDKCore, - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): Promise< [ @@ -96,7 +97,7 @@ async function $do( (value) => z.parse( operations - .ListAgentControlsApiV1AgentsAgentIdControlsGetRequest$outboundSchema, + .ListAgentControlsApiV1AgentsAgentNameControlsGetRequest$outboundSchema, value, ), "Input validation failed", @@ -108,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", @@ -127,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 72184635..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 associations, + * 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-remove-control.ts b/sdks/typescript/src/generated/funcs/agents-remove-control.ts deleted file mode 100644 index 9c8dadc3..00000000 --- a/sdks/typescript/src/generated/funcs/agents-remove-control.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { AgentControlSDKCore } from "../core.js"; -import { encodeSimple } from "../lib/encodings.js"; -import * as M from "../lib/matchers.js"; -import { compactMap } from "../lib/primitives.js"; -import { safeParse } from "../lib/schemas.js"; -import { RequestOptions } from "../lib/sdks.js"; -import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; -import { pathToFunc } from "../lib/url.js"; -import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; -import { - ConnectionError, - InvalidRequestError, - RequestAbortedError, - RequestTimeoutError, - UnexpectedClientError, -} from "../models/errors/http-client-errors.js"; -import * as errors from "../models/errors/index.js"; -import { ResponseValidationError } from "../models/errors/response-validation-error.js"; -import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; -import * as models from "../models/index.js"; -import * as operations from "../models/operations/index.js"; -import { APICall, APIPromise } from "../types/async.js"; -import { Result } from "../types/fp.js"; - -/** - * Remove direct control association from agent - * - * @remarks - * Remove a direct control association from an agent (idempotent). - */ -export function agentsRemoveControl( - client: AgentControlSDKCore, - request: - operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, - options?: RequestOptions, -): APIPromise< - Result< - models.RemoveAgentControlResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - > -> { - return new APIPromise($do( - client, - request, - options, - )); -} - -async function $do( - client: AgentControlSDKCore, - request: - operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, - options?: RequestOptions, -): Promise< - [ - Result< - models.RemoveAgentControlResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >, - APICall, - ] -> { - const parsed = safeParse( - request, - (value) => - z.parse( - operations - .RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema, - value, - ), - "Input validation failed", - ); - if (!parsed.ok) { - return [parsed, { status: "invalid" }]; - } - const payload = parsed.value; - const body = null; - - const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { - explode: false, - charEncoding: "percent", - }), - control_id: encodeSimple("control_id", payload.control_id, { - explode: false, - charEncoding: "percent", - }), - }; - - const path = pathToFunc("/api/v1/agents/{agent_id}/controls/{control_id}")( - pathParams, - ); - - const headers = new Headers(compactMap({ - Accept: "application/json", - })); - - const secConfig = await extractSecurity(client._options.apiKeyHeader); - const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; - const requestSecurity = resolveGlobalSecurity(securityInput); - - const context = { - options: client._options, - baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: - "remove_agent_control_api_v1_agents__agent_id__controls__control_id__delete", - oAuth2Scopes: null, - - resolvedSecurity: requestSecurity, - - securitySource: client._options.apiKeyHeader, - retryConfig: options?.retries - || client._options.retryConfig - || { strategy: "none" }, - retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], - }; - - const requestRes = client._createRequest(context, { - security: requestSecurity, - method: "DELETE", - baseURL: options?.serverURL, - path: path, - headers: headers, - body: body, - userAgent: client._options.userAgent, - timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, - }, options); - if (!requestRes.ok) { - return [requestRes, { status: "invalid" }]; - } - const req = requestRes.value; - - const doResult = await client._do(req, { - context, - errorCodes: ["422", "4XX", "5XX"], - retryConfig: context.retryConfig, - retryCodes: context.retryCodes, - }); - if (!doResult.ok) { - return [doResult, { status: "request-error", request: req }]; - } - const response = doResult.value; - - const responseFields = { - HttpMeta: { Response: response, Request: req }, - }; - - const [result] = await M.match< - models.RemoveAgentControlResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >( - M.json(200, models.RemoveAgentControlResponse$inboundSchema), - M.jsonErr(422, errors.HTTPValidationError$inboundSchema), - M.fail("4XX"), - M.fail("5XX"), - )(response, req, { extraFields: responseFields }); - if (!result.ok) { - return [result, { status: "complete", request: req, response }]; - } - - return [result, { status: "complete", request: req, response }]; -} diff --git a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts deleted file mode 100644 index 2fb5dc92..00000000 --- a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. - */ - -import * as z from "zod/v4-mini"; -import { AgentControlSDKCore } from "../core.js"; -import { encodeSimple } from "../lib/encodings.js"; -import * as M from "../lib/matchers.js"; -import { compactMap } from "../lib/primitives.js"; -import { safeParse } from "../lib/schemas.js"; -import { RequestOptions } from "../lib/sdks.js"; -import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; -import { pathToFunc } from "../lib/url.js"; -import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; -import { - ConnectionError, - InvalidRequestError, - RequestAbortedError, - RequestTimeoutError, - UnexpectedClientError, -} from "../models/errors/http-client-errors.js"; -import * as errors from "../models/errors/index.js"; -import { ResponseValidationError } from "../models/errors/response-validation-error.js"; -import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; -import * as models from "../models/index.js"; -import * as operations from "../models/operations/index.js"; -import { APICall, APIPromise } from "../types/async.js"; -import { Result } from "../types/fp.js"; - -/** - * Remove policy association from agent - * - * @remarks - * Remove a policy association from an agent. - * - * Idempotent for existing resources: removing a non-associated link is a no-op. - * Missing agent/policy resources still return 404. - */ -export function agentsRemovePolicy( - client: AgentControlSDKCore, - request: - operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, - options?: RequestOptions, -): APIPromise< - Result< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - > -> { - return new APIPromise($do( - client, - request, - options, - )); -} - -async function $do( - client: AgentControlSDKCore, - request: - operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, - options?: RequestOptions, -): Promise< - [ - Result< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >, - APICall, - ] -> { - const parsed = safeParse( - request, - (value) => - z.parse( - operations - .RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema, - value, - ), - "Input validation failed", - ); - if (!parsed.ok) { - return [parsed, { status: "invalid" }]; - } - const payload = parsed.value; - const body = null; - - const pathParams = { - agent_id: encodeSimple("agent_id", payload.agent_id, { - explode: false, - charEncoding: "percent", - }), - policy_id: encodeSimple("policy_id", payload.policy_id, { - explode: false, - charEncoding: "percent", - }), - }; - - const path = pathToFunc("/api/v1/agents/{agent_id}/policies/{policy_id}")( - pathParams, - ); - - const headers = new Headers(compactMap({ - Accept: "application/json", - })); - - const secConfig = await extractSecurity(client._options.apiKeyHeader); - const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; - const requestSecurity = resolveGlobalSecurity(securityInput); - - const context = { - options: client._options, - baseURL: options?.serverURL ?? client._baseURL ?? "", - operationID: - "remove_agent_policy_api_v1_agents__agent_id__policies__policy_id__delete", - oAuth2Scopes: null, - - resolvedSecurity: requestSecurity, - - securitySource: client._options.apiKeyHeader, - retryConfig: options?.retries - || client._options.retryConfig - || { strategy: "none" }, - retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], - }; - - const requestRes = client._createRequest(context, { - security: requestSecurity, - method: "DELETE", - baseURL: options?.serverURL, - path: path, - headers: headers, - body: body, - userAgent: client._options.userAgent, - timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, - }, options); - if (!requestRes.ok) { - return [requestRes, { status: "invalid" }]; - } - const req = requestRes.value; - - const doResult = await client._do(req, { - context, - errorCodes: ["422", "4XX", "5XX"], - retryConfig: context.retryConfig, - retryCodes: context.retryCodes, - }); - if (!doResult.ok) { - return [doResult, { status: "request-error", request: req }]; - } - const response = doResult.value; - - const responseFields = { - HttpMeta: { Response: response, Request: req }, - }; - - const [result] = await M.match< - models.AssocResponse, - | errors.HTTPValidationError - | AgentControlSDKError - | ResponseValidationError - | ConnectionError - | RequestAbortedError - | RequestTimeoutError - | InvalidRequestError - | UnexpectedClientError - | SDKValidationError - >( - M.json(200, models.AssocResponse$inboundSchema), - M.jsonErr(422, errors.HTTPValidationError$inboundSchema), - M.fail("4XX"), - M.fail("5XX"), - )(response, req, { extraFields: responseFields }); - if (!result.ok) { - return [result, { status: "complete", request: req, response }]; - } - - return [result, { status: "complete", request: req, response }]; -} diff --git a/sdks/typescript/src/generated/funcs/agents-add-policy.ts b/sdks/typescript/src/generated/funcs/agents-update-policy.ts similarity index 81% rename from sdks/typescript/src/generated/funcs/agents-add-policy.ts rename to sdks/typescript/src/generated/funcs/agents-update-policy.ts index c9f9d39e..7626a798 100644 --- a/sdks/typescript/src/generated/funcs/agents-add-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-update-policy.ts @@ -28,19 +28,33 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Associate policy with agent + * Assign policy to agent * * @remarks - * Associate a policy with an agent (idempotent). + * Assign a policy to an agent, replacing any existing policy assignment. + * + * The agent will immediately inherit all controls from the assigned policy. + * + * Args: + * agent_name: Agent identifier + * 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 */ -export function agentsAddPolicy( +export function agentsUpdatePolicy( client: AgentControlSDKCore, request: - operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, ): APIPromise< Result< - models.AssocResponse, + models.SetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -62,12 +76,12 @@ export function agentsAddPolicy( async function $do( client: AgentControlSDKCore, request: - operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, ): Promise< [ Result< - models.AssocResponse, + models.SetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -86,7 +100,7 @@ async function $do( (value) => z.parse( operations - .AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema, + .SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest$outboundSchema, value, ), "Input validation failed", @@ -98,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", }), @@ -108,7 +122,7 @@ async function $do( }), }; - const path = pathToFunc("/api/v1/agents/{agent_id}/policies/{policy_id}")( + const path = pathToFunc("/api/v1/agents/{agent_name}/policy/{policy_id}")( pathParams, ); @@ -124,7 +138,7 @@ async function $do( options: client._options, baseURL: options?.serverURL ?? client._baseURL ?? "", operationID: - "add_agent_policy_api_v1_agents__agent_id__policies__policy_id__post", + "set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post", oAuth2Scopes: null, resolvedSecurity: requestSecurity, @@ -167,7 +181,7 @@ async function $do( }; const [result] = await M.match< - models.AssocResponse, + models.SetPolicyResponse, | errors.HTTPValidationError | AgentControlSDKError | ResponseValidationError @@ -178,7 +192,7 @@ async function $do( | UnexpectedClientError | SDKValidationError >( - M.json(200, models.AssocResponse$inboundSchema), + M.json(200, models.SetPolicyResponse$inboundSchema), M.jsonErr(422, errors.HTTPValidationError$inboundSchema), M.fail("4XX"), M.fail("5XX"), 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/controls-delete.ts b/sdks/typescript/src/generated/funcs/controls-delete.ts index ea973a24..c3b65d39 100644 --- a/sdks/typescript/src/generated/funcs/controls-delete.ts +++ b/sdks/typescript/src/generated/funcs/controls-delete.ts @@ -33,7 +33,7 @@ import { Result } from "../types/fp.js"; * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy or agent. + * By default, deletion fails if the control is associated with any policy. * Use force=true to automatically dissociate and delete. * * Args: @@ -42,7 +42,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and dissociation details + * DeleteControlResponse with success flag and list of dissociated policies * * Raises: * HTTPException 404: Control not found 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/models/agent-controls-response.ts b/sdks/typescript/src/generated/models/agent-controls-response.ts index 22a591ee..0668702c 100644 --- a/sdks/typescript/src/generated/models/agent-controls-response.ts +++ b/sdks/typescript/src/generated/models/agent-controls-response.ts @@ -10,7 +10,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; export type AgentControlsResponse = { /** - * List of active controls associated with the agent + * List of controls associated with the agent via its policy */ controls: Array; }; 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 4758c8d2..a2cccd70 100644 --- a/sdks/typescript/src/generated/models/agent-summary.ts +++ b/sdks/typescript/src/generated/models/agent-summary.ts @@ -14,15 +14,11 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type AgentSummary = { /** - * Number of active controls for this agent + * Number of active controls from agent's policy */ activeControlsCount: number; /** - * UUID of the agent - */ - agentId: string; - /** - * Human-readable name of the agent + * Unique identifier of the agent */ agentName: string; /** @@ -34,9 +30,9 @@ export type AgentSummary = { */ evaluatorCount: number; /** - * IDs of policies associated with the agent + * ID of assigned policy, if any */ - policyIds?: Array | undefined; + policyId?: number | null | undefined; /** * Number of steps registered with the agent */ @@ -48,21 +44,19 @@ 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), - policy_ids: types.optional(z.array(types.number())), + policy_id: z.optional(z.nullable(types.number())), step_count: z._default(types.number(), 0), }), z.transform((v) => { return remap$(v, { "active_controls_count": "activeControlsCount", - "agent_id": "agentId", "agent_name": "agentName", "created_at": "createdAt", "evaluator_count": "evaluatorCount", - "policy_ids": "policyIds", + "policy_id": "policyId", "step_count": "stepCount", }); }), 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-summary.ts b/sdks/typescript/src/generated/models/control-summary.ts index 7e89a07f..9e0356a5 100644 --- a/sdks/typescript/src/generated/models/control-summary.ts +++ b/sdks/typescript/src/generated/models/control-summary.ts @@ -50,10 +50,6 @@ export type ControlSummary = { * Agent using this control */ usedByAgent?: AgentRef | null | undefined; - /** - * Number of unique agents using this control - */ - usedByAgentsCount: number; }; /** @internal */ @@ -71,13 +67,11 @@ export const ControlSummary$inboundSchema: z.ZodMiniType< step_types: z.optional(z.nullable(z.array(types.string()))), tags: types.optional(z.array(types.string())), used_by_agent: z.optional(z.nullable(AgentRef$inboundSchema)), - used_by_agents_count: z._default(types.number(), 0), }), z.transform((v) => { return remap$(v, { "step_types": "stepTypes", "used_by_agent": "usedByAgent", - "used_by_agents_count": "usedByAgentsCount", }); }), ); diff --git a/sdks/typescript/src/generated/models/delete-control-response.ts b/sdks/typescript/src/generated/models/delete-control-response.ts index e57cbf83..623abd95 100644 --- a/sdks/typescript/src/generated/models/delete-control-response.ts +++ b/sdks/typescript/src/generated/models/delete-control-response.ts @@ -13,14 +13,10 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; * Response for deleting a control. */ export type DeleteControlResponse = { - /** - * Agent IDs the control was removed from before deletion - */ - dissociatedFromAgents?: Array | undefined; /** * Policy IDs the control was removed from before deletion */ - dissociatedFromPolicies?: Array | undefined; + dissociatedFrom?: Array | undefined; /** * Whether the control was deleted */ @@ -33,14 +29,12 @@ export const DeleteControlResponse$inboundSchema: z.ZodMiniType< unknown > = z.pipe( z.object({ - dissociated_from_agents: types.optional(z.array(types.string())), - dissociated_from_policies: types.optional(z.array(types.number())), + dissociated_from: types.optional(z.array(types.number())), success: types.boolean(), }), z.transform((v) => { return remap$(v, { - "dissociated_from_agents": "dissociatedFromAgents", - "dissociated_from_policies": "dissociatedFromPolicies", + "dissociated_from": "dissociatedFrom", }); }), ); diff --git a/sdks/typescript/src/generated/models/delete-policy-response.ts b/sdks/typescript/src/generated/models/delete-policy-response.ts new file mode 100644 index 00000000..d37ef18e --- /dev/null +++ b/sdks/typescript/src/generated/models/delete-policy-response.ts @@ -0,0 +1,34 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import * as types from "../types/primitives.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; + +export type DeletePolicyResponse = { + /** + * Whether the policy was successfully removed + */ + success: boolean; +}; + +/** @internal */ +export const DeletePolicyResponse$inboundSchema: z.ZodMiniType< + DeletePolicyResponse, + unknown +> = z.object({ + success: types.boolean(), +}); + +export function deletePolicyResponseFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => DeletePolicyResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'DeletePolicyResponse' from JSON`, + ); +} 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/get-agent-policies-response.ts b/sdks/typescript/src/generated/models/get-policy-response.ts similarity index 51% rename from sdks/typescript/src/generated/models/get-agent-policies-response.ts rename to sdks/typescript/src/generated/models/get-policy-response.ts index 3e9413e0..fd8437df 100644 --- a/sdks/typescript/src/generated/models/get-agent-policies-response.ts +++ b/sdks/typescript/src/generated/models/get-policy-response.ts @@ -9,34 +9,34 @@ import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; -export type GetAgentPoliciesResponse = { +export type GetPolicyResponse = { /** - * IDs of policies associated with the agent + * Identifier of the policy assigned to the agent */ - policyIds?: Array | undefined; + policyId: number; }; /** @internal */ -export const GetAgentPoliciesResponse$inboundSchema: z.ZodMiniType< - GetAgentPoliciesResponse, +export const GetPolicyResponse$inboundSchema: z.ZodMiniType< + GetPolicyResponse, unknown > = z.pipe( z.object({ - policy_ids: types.optional(z.array(types.number())), + policy_id: types.number(), }), z.transform((v) => { return remap$(v, { - "policy_ids": "policyIds", + "policy_id": "policyId", }); }), ); -export function getAgentPoliciesResponseFromJSON( +export function getPolicyResponseFromJSON( jsonString: string, -): SafeParseResult { +): SafeParseResult { return safeParse( jsonString, - (x) => GetAgentPoliciesResponse$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'GetAgentPoliciesResponse' from JSON`, + (x) => GetPolicyResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'GetPolicyResponse' from JSON`, ); } diff --git a/sdks/typescript/src/generated/models/index.ts b/sdks/typescript/src/generated/models/index.ts index 936d6e2b..af193ed3 100644 --- a/sdks/typescript/src/generated/models/index.ts +++ b/sdks/typescript/src/generated/models/index.ts @@ -28,6 +28,7 @@ export * from "./create-policy-request.js"; export * from "./create-policy-response.js"; export * from "./delete-control-response.js"; export * from "./delete-evaluator-config-response.js"; +export * from "./delete-policy-response.js"; export * from "./evaluation-request.js"; export * from "./evaluation-response.js"; export * from "./evaluator-config-item.js"; @@ -38,11 +39,11 @@ export * from "./evaluator-schema.js"; export * from "./evaluator-spec.js"; export * from "./event-query-request.js"; export * from "./event-query-response.js"; -export * from "./get-agent-policies-response.js"; export * from "./get-agent-response.js"; export * from "./get-control-data-response.js"; export * from "./get-control-response.js"; export * from "./get-policy-controls-response.js"; +export * from "./get-policy-response.js"; export * from "./health-response.js"; export * from "./init-agent-evaluator-removal.js"; export * from "./init-agent-overwrite-changes.js"; @@ -57,10 +58,10 @@ export * from "./patch-agent-request.js"; export * from "./patch-agent-response.js"; export * from "./patch-control-request.js"; export * from "./patch-control-response.js"; -export * from "./remove-agent-control-response.js"; export * from "./security.js"; export * from "./set-control-data-request.js"; export * from "./set-control-data-response.js"; +export * from "./set-policy-response.js"; export * from "./stats-response.js"; export * from "./stats-totals.js"; export * from "./steering-context.js"; 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/init-agent-response.ts b/sdks/typescript/src/generated/models/init-agent-response.ts index 76ade4de..c2e00b02 100644 --- a/sdks/typescript/src/generated/models/init-agent-response.ts +++ b/sdks/typescript/src/generated/models/init-agent-response.ts @@ -19,7 +19,7 @@ import { */ export type InitAgentResponse = { /** - * Active protection controls for the agent + * Active protection controls for the agent (if policy assigned) */ controls?: Array | undefined; /** diff --git a/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-id-post.ts deleted file mode 100644 index 320fddef..00000000 --- a/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-id-controls-control-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 AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest = { - agentId: string; - controlId: number; -}; - -/** @internal */ -export type AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$Outbound = - { - agent_id: string; - control_id: number; - }; - -/** @internal */ -export const AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema: - z.ZodMiniType< - AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$Outbound, - AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest - > = z.pipe( - z.object({ - agentId: z.string(), - controlId: z.int(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - controlId: "control_id", - }); - }), - ); - -export function addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequestToJSON( - addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest: - AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, -): string { - return JSON.stringify( - AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest$outboundSchema - .parse(addAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.ts deleted file mode 100644 index bbc38700..00000000 --- a/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-id-policies-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 AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest = { - agentId: string; - policyId: number; -}; - -/** @internal */ -export type AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$Outbound = - { - agent_id: string; - policy_id: number; - }; - -/** @internal */ -export const AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema: - z.ZodMiniType< - AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$Outbound, - AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest - > = z.pipe( - z.object({ - agentId: z.string(), - policyId: z.int(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - policyId: "policy_id", - }); - }), - ); - -export function addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequestToJSON( - addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest: - AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, -): string { - return JSON.stringify( - AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest$outboundSchema - .parse(addAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest), - ); -} 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/delete-control-api-v1-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts index b016ec01..6d3935ba 100644 --- a/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts +++ b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts @@ -8,7 +8,7 @@ import { remap as remap$ } from "../../lib/primitives.js"; export type DeleteControlApiV1ControlsControlIdDeleteRequest = { controlId: number; /** - * If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. + * If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. */ force?: boolean | undefined; }; 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-policies-api-v1-agents-agent-id-policies-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-get.ts deleted file mode 100644 index 94a18506..00000000 --- a/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-id-policies-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 GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest = { - agentId: string; -}; - -/** @internal */ -export type GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$Outbound = { - agent_id: string; -}; - -/** @internal */ -export const GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema: - z.ZodMiniType< - GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$Outbound, - GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest - > = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequestToJSON( - getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest: - GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, -): string { - return JSON.stringify( - GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest$outboundSchema.parse( - getAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, - ), - ); -} 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/index.ts b/sdks/typescript/src/generated/models/operations/index.ts index 96194f2b..8e6bebc2 100644 --- a/sdks/typescript/src/generated/models/operations/index.ts +++ b/sdks/typescript/src/generated/models/operations/index.ts @@ -2,31 +2,28 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -export * from "./add-agent-control-api-v1-agents-agent-id-controls-control-id-post.js"; -export * from "./add-agent-policy-api-v1-agents-agent-id-policies-policy-id-post.js"; 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-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-policies-api-v1-agents-agent-id-policies-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-agent-control-api-v1-agents-agent-id-controls-control-id-delete.js"; -export * from "./remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.js"; -export * from "./remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.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-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/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.ts deleted file mode 100644 index 8d53fc48..00000000 --- a/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-id-controls-control-id-delete.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 RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest = - { - agentId: string; - controlId: number; - }; - -/** @internal */ -export type RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$Outbound = - { - agent_id: string; - control_id: number; - }; - -/** @internal */ -export const RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema: - z.ZodMiniType< - RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$Outbound, - RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest - > = z.pipe( - z.object({ - agentId: z.string(), - controlId: z.int(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - controlId: "control_id", - }); - }), - ); - -export function removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequestToJSON( - removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest: - RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, -): string { - return JSON.stringify( - RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest$outboundSchema - .parse( - removeAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, - ), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.ts deleted file mode 100644 index ac14a1ad..00000000 --- a/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-id-policies-policy-id-delete.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 RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest = { - agentId: string; - policyId: number; -}; - -/** @internal */ -export type RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$Outbound = - { - agent_id: string; - policy_id: number; - }; - -/** @internal */ -export const RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema: - z.ZodMiniType< - RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$Outbound, - RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest - > = z.pipe( - z.object({ - agentId: z.string(), - policyId: z.int(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - policyId: "policy_id", - }); - }), - ); - -export function removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequestToJSON( - removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest: - RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, -): string { - return JSON.stringify( - RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest$outboundSchema - .parse(removeAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest), - ); -} diff --git a/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts b/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-delete.ts deleted file mode 100644 index 59a64b75..00000000 --- a/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-id-policies-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 RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest = { - agentId: string; -}; - -/** @internal */ -export type RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$Outbound = - { - agent_id: string; - }; - -/** @internal */ -export const RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema: - z.ZodMiniType< - RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$Outbound, - RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest - > = z.pipe( - z.object({ - agentId: z.string(), - }), - z.transform((v) => { - return remap$(v, { - agentId: "agent_id", - }); - }), - ); - -export function removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequestToJSON( - removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest: - RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, -): string { - return JSON.stringify( - RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest$outboundSchema - .parse(removeAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest), - ); -} 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/remove-agent-control-response.ts b/sdks/typescript/src/generated/models/remove-agent-control-response.ts deleted file mode 100644 index e0c61fde..00000000 --- a/sdks/typescript/src/generated/models/remove-agent-control-response.ts +++ /dev/null @@ -1,56 +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 { safeParse } from "../lib/schemas.js"; -import { Result as SafeParseResult } from "../types/fp.js"; -import * as types from "../types/primitives.js"; -import { SDKValidationError } from "./errors/sdk-validation-error.js"; - -/** - * Response for removing a direct agent-control association. - */ -export type RemoveAgentControlResponse = { - /** - * True if the control remains active via policy association(s) - */ - controlStillActive: boolean; - /** - * True if a direct agent-control link was removed - */ - removedDirectAssociation: boolean; - /** - * Whether the request succeeded - */ - success: boolean; -}; - -/** @internal */ -export const RemoveAgentControlResponse$inboundSchema: z.ZodMiniType< - RemoveAgentControlResponse, - unknown -> = z.pipe( - z.object({ - control_still_active: types.boolean(), - removed_direct_association: types.boolean(), - success: types.boolean(), - }), - z.transform((v) => { - return remap$(v, { - "control_still_active": "controlStillActive", - "removed_direct_association": "removedDirectAssociation", - }); - }), -); - -export function removeAgentControlResponseFromJSON( - jsonString: string, -): SafeParseResult { - return safeParse( - jsonString, - (x) => RemoveAgentControlResponse$inboundSchema.parse(JSON.parse(x)), - `Failed to parse 'RemoveAgentControlResponse' from JSON`, - ); -} diff --git a/sdks/typescript/src/generated/models/set-policy-response.ts b/sdks/typescript/src/generated/models/set-policy-response.ts new file mode 100644 index 00000000..7fa3bfe6 --- /dev/null +++ b/sdks/typescript/src/generated/models/set-policy-response.ts @@ -0,0 +1,47 @@ +/* + * 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 { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import * as types from "../types/primitives.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; + +export type SetPolicyResponse = { + /** + * Previous policy id if one was replaced + */ + oldPolicyId?: number | null | undefined; + /** + * Whether the policy was successfully assigned + */ + success: boolean; +}; + +/** @internal */ +export const SetPolicyResponse$inboundSchema: z.ZodMiniType< + SetPolicyResponse, + unknown +> = z.pipe( + z.object({ + old_policy_id: z.optional(z.nullable(types.number())), + success: types.boolean(), + }), + z.transform((v) => { + return remap$(v, { + "old_policy_id": "oldPolicyId", + }); + }), +); + +export function setPolicyResponseFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => SetPolicyResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'SetPolicyResponse' from JSON`, + ); +} diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index 84f34b29..98c0c874 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -2,18 +2,15 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ -import { agentsAddControl } from "../funcs/agents-add-control.js"; -import { agentsAddPolicy } from "../funcs/agents-add-policy.js"; +import { agentsDeletePolicy } from "../funcs/agents-delete-policy.js"; import { agentsGetEvaluator } from "../funcs/agents-get-evaluator.js"; -import { agentsGetPolicies } from "../funcs/agents-get-policies.js"; +import { agentsGetPolicy } from "../funcs/agents-get-policy.js"; import { agentsGet } from "../funcs/agents-get.js"; import { agentsInit } from "../funcs/agents-init.js"; import { agentsListControls } from "../funcs/agents-list-controls.js"; import { agentsListEvaluators } from "../funcs/agents-list-evaluators.js"; import { agentsList } from "../funcs/agents-list.js"; -import { agentsRemoveAllAgentPolicies } from "../funcs/agents-remove-all-agent-policies.js"; -import { agentsRemoveControl } from "../funcs/agents-remove-control.js"; -import { agentsRemovePolicy } from "../funcs/agents-remove-policy.js"; +import { agentsUpdatePolicy } from "../funcs/agents-update-policy.js"; import { agentsUpdate } from "../funcs/agents-update.js"; import { ClientSDK, RequestOptions } from "../lib/sdks.js"; import * as models from "../models/index.js"; @@ -27,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 associations, + * 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) @@ -58,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 @@ -71,11 +66,7 @@ export class Agents extends ClientSDK { * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls - * - * Raises: - * HTTPException 409: Agent name exists with different UUID - * HTTPException 500: Database error during creation/update + * InitAgentResponse with created flag and active controls (if policy assigned) */ async init( request: models.InitAgentRequest, @@ -97,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: @@ -108,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( @@ -128,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) * @@ -140,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( @@ -156,20 +147,21 @@ export class Agents extends ClientSDK { * @remarks * List all protection controls active for an agent. * - * Controls include the union of policy-derived and directly associated controls. + * Controls are inherited from the agent's assigned policy. + * 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: - * AgentControlsResponse with list of active controls + * AgentControlsResponse with list of controls (empty if no policy) * * Raises: * HTTPException 404: Agent not found */ async listControls( - request: operations.ListAgentControlsApiV1AgentsAgentIdControlsGetRequest, + request: operations.ListAgentControlsApiV1AgentsAgentNameControlsGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsListControls( @@ -179,42 +171,6 @@ export class Agents extends ClientSDK { )); } - /** - * Remove direct control association from agent - * - * @remarks - * Remove a direct control association from an agent (idempotent). - */ - async removeControl( - request: - operations.RemoveAgentControlApiV1AgentsAgentIdControlsControlIdDeleteRequest, - options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsRemoveControl( - this, - request, - options, - )); - } - - /** - * Associate control directly with agent - * - * @remarks - * Associate a control directly with an agent (idempotent). - */ - async addControl( - request: - operations.AddAgentControlApiV1AgentsAgentIdControlsControlIdPostRequest, - options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsAddControl( - this, - request, - options, - )); - } - /** * List agent's registered evaluator schemas * @@ -226,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) @@ -239,7 +195,7 @@ export class Agents extends ClientSDK { */ async listEvaluators( request: - operations.ListAgentEvaluatorsApiV1AgentsAgentIdEvaluatorsGetRequest, + operations.ListAgentEvaluatorsApiV1AgentsAgentNameEvaluatorsGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsListEvaluators( @@ -256,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) * @@ -268,7 +224,7 @@ export class Agents extends ClientSDK { */ async getEvaluator( request: - operations.GetAgentEvaluatorApiV1AgentsAgentIdEvaluatorsEvaluatorNameGetRequest, + operations.GetAgentEvaluatorApiV1AgentsAgentNameEvaluatorsEvaluatorNameGetRequest, options?: RequestOptions, ): Promise { return unwrapAsync(agentsGetEvaluator( @@ -279,17 +235,30 @@ export class Agents extends ClientSDK { } /** - * Remove all policy associations from agent + * Remove agent's policy assignment * * @remarks - * Remove all policy associations from an agent. + * Remove the policy assignment from an agent. + * + * The agent will no longer have any protection controls active. + * + * Args: + * agent_name: Agent identifier + * 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 */ - async removeAllAgentPolicies( + async deletePolicy( request: - operations.RemoveAllAgentPoliciesApiV1AgentsAgentIdPoliciesDeleteRequest, + operations.DeleteAgentPolicyApiV1AgentsAgentNamePolicyDeleteRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsRemoveAllAgentPolicies( + ): Promise { + return unwrapAsync(agentsDeletePolicy( this, request, options, @@ -297,37 +266,26 @@ export class Agents extends ClientSDK { } /** - * List policies associated with agent + * Get agent's assigned policy * * @remarks - * List policy IDs associated with an agent. - */ - async getPolicies( - request: operations.GetAgentPoliciesApiV1AgentsAgentIdPoliciesGetRequest, - options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsGetPolicies( - this, - request, - options, - )); - } - - /** - * Remove policy association from agent + * Retrieve the policy currently assigned to an agent. * - * @remarks - * Remove a policy association from an agent. + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * GetPolicyResponse with policy ID * - * Idempotent for existing resources: removing a non-associated link is a no-op. - * Missing agent/policy resources still return 404. + * Raises: + * HTTPException 404: Agent not found or agent has no policy assigned */ - async removePolicy( - request: - operations.RemoveAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdDeleteRequest, + async getPolicy( + request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsRemovePolicy( + ): Promise { + return unwrapAsync(agentsGetPolicy( this, request, options, @@ -335,17 +293,31 @@ export class Agents extends ClientSDK { } /** - * Associate policy with agent + * Assign policy to agent * * @remarks - * Associate a policy with an agent (idempotent). + * Assign a policy to an agent, replacing any existing policy assignment. + * + * The agent will immediately inherit all controls from the assigned policy. + * + * Args: + * agent_name: Agent identifier + * 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 */ - async addPolicy( + async updatePolicy( request: - operations.AddAgentPolicyApiV1AgentsAgentIdPoliciesPolicyIdPostRequest, + operations.SetAgentPolicyApiV1AgentsAgentNamePolicyPolicyIdPostRequest, options?: RequestOptions, - ): Promise { - return unwrapAsync(agentsAddPolicy( + ): Promise { + return unwrapAsync(agentsUpdatePolicy( this, request, options, diff --git a/sdks/typescript/src/generated/sdk/controls.ts b/sdks/typescript/src/generated/sdk/controls.ts index c32be744..e4871a8a 100644 --- a/sdks/typescript/src/generated/sdk/controls.ts +++ b/sdks/typescript/src/generated/sdk/controls.ts @@ -113,7 +113,7 @@ export class Controls extends ClientSDK { * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy or agent. + * By default, deletion fails if the control is associated with any policy. * Use force=true to automatically dissociate and delete. * * Args: @@ -122,7 +122,7 @@ export class Controls extends ClientSDK { * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and dissociation details + * DeleteControlResponse with success flag and list of dissociated policies * * Raises: * HTTPException 404: Control not found 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/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/tests/test_agents_additional.py b/server/tests/test_agents_additional.py index 2297e9a5..8c8085f8 100644 --- a/server/tests/test_agents_additional.py +++ b/server/tests/test_agents_additional.py @@ -14,12 +14,11 @@ def _init_agent( client: TestClient, *, - agent_id: str | None = None, agent_name: str | None = None, steps: list[dict] | None = None, evaluators: list[dict] | None = None, ) -> tuple[str, str]: - name = (agent_name or agent_id or f"agent-{uuid.uuid4().hex[:12]}").lower() + name = (agent_name or f"agent-{uuid.uuid4().hex[:12]}").lower() if len(name) < 10: name = f"{name}-agent".replace("--", "-") payload = { @@ -58,10 +57,10 @@ def test_list_agent_evaluators_pagination_and_get(client: TestClient) -> None: {"name": "eval-b", "description": "b", "config_schema": {"type": "object"}}, {"name": "eval-c", "description": "c", "config_schema": {}}, ] - agent_id, _ = _init_agent(client, evaluators=evaluators) + agent_name, _ = _init_agent(client, evaluators=evaluators) # When: listing with pagination - resp = client.get(f"/api/v1/agents/{agent_id}/evaluators", params={"limit": 2}) + resp = client.get(f"/api/v1/agents/{agent_name}/evaluators", params={"limit": 2}) assert resp.status_code == 200 body = resp.json() # Then: first page returns two items and a next cursor @@ -71,7 +70,7 @@ def test_list_agent_evaluators_pagination_and_get(client: TestClient) -> None: # When: fetching next page using cursor resp2 = client.get( - f"/api/v1/agents/{agent_id}/evaluators", + f"/api/v1/agents/{agent_name}/evaluators", params={"limit": 2, "cursor": body["pagination"]["next_cursor"]}, ) assert resp2.status_code == 200 @@ -81,7 +80,7 @@ def test_list_agent_evaluators_pagination_and_get(client: TestClient) -> None: assert [e["name"] for e in body2["evaluators"]] == ["eval-c"] # When: getting a specific evaluator - get_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators/eval-b") + get_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators/eval-b") assert get_resp.status_code == 200 evaluator = get_resp.json() # Then: evaluator details are returned @@ -95,16 +94,16 @@ def test_list_agent_evaluators_invalid_cursor_returns_first_page(client: TestCli {"name": "eval-a", "description": "a", "config_schema": {}}, {"name": "eval-b", "description": "b", "config_schema": {}}, ] - agent_id, _ = _init_agent(client, evaluators=evaluators) + agent_name, _ = _init_agent(client, evaluators=evaluators) # When: listing without cursor - resp = client.get(f"/api/v1/agents/{agent_id}/evaluators", params={"limit": 1}) + resp = client.get(f"/api/v1/agents/{agent_name}/evaluators", params={"limit": 1}) assert resp.status_code == 200 base = resp.json() # When: listing with an invalid cursor resp2 = client.get( - f"/api/v1/agents/{agent_id}/evaluators", + f"/api/v1/agents/{agent_name}/evaluators", params={"limit": 1, "cursor": "does-not-exist"}, ) assert resp2.status_code == 200 @@ -123,12 +122,12 @@ def test_init_agent_preserves_existing_steps_when_missing_from_payload( {"type": "tool", "name": "tool-a", "input_schema": {}, "output_schema": {}}, {"type": "tool", "name": "tool-b", "input_schema": {}, "output_schema": {}}, ] - agent_id, agent_name = _init_agent(client, steps=steps) + agent_name, agent_name = _init_agent(client, steps=steps) # When: re-initializing with only one of the steps payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -140,7 +139,7 @@ def test_init_agent_preserves_existing_steps_when_missing_from_payload( assert resp.status_code == 200 # Then: the missing step is preserved (initAgent only adds) - 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 step_names = {step["name"] for step in get_resp.json()["steps"]} assert step_names == {"tool-a", "tool-b"} @@ -148,10 +147,10 @@ def test_init_agent_preserves_existing_steps_when_missing_from_payload( def test_get_agent_evaluator_not_found(client: TestClient) -> None: # Given: an existing agent with no matching evaluator - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) # When: requesting a missing evaluator - resp = client.get(f"/api/v1/agents/{agent_id}/evaluators/missing") + resp = client.get(f"/api/v1/agents/{agent_name}/evaluators/missing") # Then: 404 not found assert resp.status_code == 404 @@ -180,11 +179,11 @@ def test_patch_agent_remove_steps_and_evaluators(client: TestClient) -> None: {"name": "eval-a", "description": "a", "config_schema": {}}, {"name": "eval-b", "description": "b", "config_schema": {}}, ] - agent_id, _ = _init_agent(client, steps=steps, evaluators=evaluators) + agent_name, _ = _init_agent(client, steps=steps, evaluators=evaluators) # When: removing one step and one evaluator resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={ "remove_steps": [{"type": "tool", "name": "tool-a"}], "remove_evaluators": ["eval-b"], @@ -198,7 +197,7 @@ def test_patch_agent_remove_steps_and_evaluators(client: TestClient) -> None: assert body["evaluators_removed"] == ["eval-b"] # Then: agent data reflects removal - 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 data = get_resp.json() assert {s["name"] for s in data["steps"]} == {"tool-b"} @@ -218,7 +217,7 @@ def test_patch_agent_remove_evaluator_in_use_conflict(client: TestClient) -> Non }, } ] - agent_id, agent_name = _init_agent(client, evaluators=evaluators) + agent_name, agent_name = _init_agent(client, evaluators=evaluators) control_payload = deepcopy(VALID_CONTROL_PAYLOAD) control_payload["evaluator"] = { @@ -230,12 +229,12 @@ def test_patch_agent_remove_evaluator_in_use_conflict(client: TestClient) -> Non policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign.status_code == 200 # When: attempting to remove evaluator in use resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_evaluators": ["custom"]}, ) @@ -281,7 +280,7 @@ def test_init_agent_rejects_builtin_evaluator_name(client: TestClient) -> None: # Given: a payload that registers an evaluator matching a built-in name payload = { "agent": { - "agent_id": str(uuid.uuid4()), + "agent_name": str(uuid.uuid4()), "agent_name": f"agent-{uuid.uuid4().hex[:12]}", "agent_description": "desc", "agent_version": "1.0", @@ -346,14 +345,14 @@ def test_list_agent_controls_corrupted_control_data_returns_422( client: TestClient, ) -> None: # Given: an agent with a policy that includes a control - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) control_payload = deepcopy(VALID_CONTROL_PAYLOAD) control_payload["evaluator"] = {"name": "regex", "config": {"pattern": "x"}} control_id = _create_control_with_data(client, control_payload) policy_id = _create_policy(client) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign.status_code == 200 # And: the control data is corrupted in the DB @@ -364,7 +363,7 @@ def test_list_agent_controls_corrupted_control_data_returns_422( ) # When: listing agent controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") # Then: corrupted data error is returned assert resp.status_code == 422 @@ -393,15 +392,15 @@ def test_list_agents_invalid_cursor_returns_first_page(client: TestClient) -> No def test_list_agent_evaluators_corrupted_data_returns_empty(client: TestClient) -> None: # Given: an agent with corrupted stored data - agent_id, _ = _init_agent(client, evaluators=[{"name": "eval-a", "config_schema": {}}]) + agent_name, _ = _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 name = :id"), - {"data": "{\"bad\": \"data\"}", "id": agent_id}, + {"data": "{\"bad\": \"data\"}", "id": agent_name}, ) # When: listing evaluator schemas - resp = client.get(f"/api/v1/agents/{agent_id}/evaluators") + resp = client.get(f"/api/v1/agents/{agent_name}/evaluators") # Then: empty list is returned assert resp.status_code == 200 @@ -412,7 +411,7 @@ def test_list_agent_evaluators_corrupted_data_returns_empty(client: TestClient) def test_set_agent_policy_rejects_corrupted_agent_data(client: TestClient) -> None: # Given: an agent with corrupted stored data and a policy with a control - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) policy_id = _create_policy(client) control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") @@ -421,11 +420,11 @@ 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 name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -436,7 +435,7 @@ def test_set_agent_policy_rejects_corrupted_agent_data(client: TestClient) -> No def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> None: # Given: an agent with no evaluators and a control referencing a missing evaluator - agent_id, agent_name = _init_agent(client) + agent_name, agent_name = _init_agent(client) policy_id = _create_policy(client) control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") @@ -454,7 +453,7 @@ def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -465,7 +464,7 @@ def test_set_agent_policy_rejects_missing_agent_evaluator(client: TestClient) -> def test_set_agent_policy_rejects_invalid_agent_evaluator_config(client: TestClient) -> None: # Given: an agent with an evaluator schema requiring \"pattern\" - agent_id, agent_name = _init_agent( + agent_name, agent_name = _init_agent( client, evaluators=[ { @@ -496,7 +495,7 @@ def test_set_agent_policy_rejects_invalid_agent_evaluator_config(client: TestCli ) # When: assigning policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: incompatible controls error is returned assert resp.status_code == 400 @@ -531,10 +530,10 @@ def test_delete_agent_policy_agent_not_found(client: TestClient) -> None: def test_delete_agent_policy_no_policy_assigned_returns_404(client: TestClient) -> None: # Given: an agent with no policy assigned - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) # When: deleting policy - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_name}/policy") # Then: policy not found error is returned assert resp.status_code == 404 @@ -543,7 +542,7 @@ def test_delete_agent_policy_no_policy_assigned_returns_404(client: TestClient) def test_list_agents_corrupted_data_sets_zero_counts(client: TestClient) -> None: # Given: an agent with corrupted data stored in the DB - agent_id, _ = _init_agent( + agent_name, _ = _init_agent( client, steps=[{"type": "tool", "name": "tool-a", "input_schema": {}, "output_schema": {}}], evaluators=[{"name": "eval-a", "config_schema": {}}], @@ -551,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 name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) # When: listing agents @@ -560,22 +559,22 @@ 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_name"]: a for a in resp.json()["agents"]} - agent = agents[agent_id] + agent = agents[agent_name] assert agent["step_count"] == 0 assert agent["evaluator_count"] == 0 def test_get_agent_corrupted_data_returns_422(client: TestClient) -> None: # Given: an agent with corrupted stored data - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) with engine.begin() as conn: conn.execute( text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) # When: fetching the agent - resp = client.get(f"/api/v1/agents/{agent_id}") + resp = client.get(f"/api/v1/agents/{agent_name}") # Then: corrupted data error is returned assert resp.status_code == 422 @@ -584,16 +583,16 @@ def test_get_agent_corrupted_data_returns_422(client: TestClient) -> None: def test_get_agent_corrupted_metadata_returns_422(client: TestClient) -> None: # Given: an agent with invalid agent_metadata payload - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) corrupted = {"agent_metadata": {}, "steps": [], "evaluators": []} with engine.begin() as conn: conn.execute( text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), - {"data": json.dumps(corrupted), "id": agent_id}, + {"data": json.dumps(corrupted), "id": agent_name}, ) # When: fetching the agent - resp = client.get(f"/api/v1/agents/{agent_id}") + resp = client.get(f"/api/v1/agents/{agent_name}") # Then: corrupted metadata error is returned assert resp.status_code == 422 @@ -602,9 +601,9 @@ def test_get_agent_corrupted_metadata_returns_422(client: TestClient) -> None: def test_get_agent_policy_missing_policy_returns_404(client: TestClient) -> None: # Given: an agent assigned to a policy that cannot be found - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) policy_id = _create_policy(client) - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign.status_code == 200 from agent_control_server.db import get_async_db @@ -619,7 +618,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.name == agent_id) + select(AgentModel).where(AgentModel.name == agent_name) ) .scalars() .first() @@ -640,7 +639,7 @@ async def mock_db_missing_policy() -> AsyncGenerator[AsyncSession, None]: # When: retrieving the agent policy and policy lookup returns None app.dependency_overrides[get_async_db] = mock_db_missing_policy try: - resp = client.get(f"/api/v1/agents/{agent_id}/policy") + resp = client.get(f"/api/v1/agents/{agent_name}/policy") finally: app.dependency_overrides.clear() @@ -651,7 +650,7 @@ async def mock_db_missing_policy() -> AsyncGenerator[AsyncSession, None]: def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> None: # Given: an agent and a policy with a control that has no data configured - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) policy_id = _create_policy(client) control_resp = client.put("/api/v1/controls", json={"name": f"control-{uuid.uuid4()}"}) assert control_resp.status_code == 200 @@ -660,7 +659,7 @@ def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> Non assert assoc.status_code == 200 # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: assignment succeeds because empty data is ignored during validation assert resp.status_code == 200 @@ -669,7 +668,7 @@ def test_set_agent_policy_skips_controls_without_data(client: TestClient) -> Non def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClient) -> None: # Given: an agent and a policy with a control missing evaluator name - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) policy_id = _create_policy(client) control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") @@ -682,7 +681,7 @@ def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClie ) # When: assigning the policy to the agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: assignment succeeds because evaluator name is missing assert resp.status_code == 200 @@ -691,7 +690,7 @@ def test_set_agent_policy_skips_controls_without_evaluator_name(client: TestClie def test_list_agents_includes_active_controls_count(client: TestClient) -> None: # Given: an agent assigned to a policy with two controls - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) policy_id = _create_policy(client) control_ids = [ _create_control_with_data(client, VALID_CONTROL_PAYLOAD), @@ -700,7 +699,7 @@ def test_list_agents_includes_active_controls_count(client: TestClient) -> None: for control_id in control_ids: assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign.status_code == 200 # When: listing agents @@ -735,10 +734,9 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -754,7 +752,7 @@ def test_init_agent_adds_new_evaluator(client: TestClient) -> None: "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -766,7 +764,7 @@ def test_init_agent_adds_new_evaluator(client: TestClient) -> None: # Then: both evaluators are present assert resp2.status_code == 200 - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") names = {e["name"] for e in get_resp.json()["evaluators"]} assert names == {"eval-a", "eval-b"} @@ -774,12 +772,11 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name init_resp = client.post( "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -794,7 +791,7 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> control_id = _create_control_with_data(client, VALID_CONTROL_PAYLOAD) assoc = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") assert assoc.status_code == 200 - assign = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign.status_code == 200 # When: re-initializing the agent with the same UUID @@ -802,7 +799,7 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -821,16 +818,16 @@ def test_init_agent_returns_controls_when_policy_assigned(client: TestClient) -> def test_patch_agent_corrupted_data_returns_422(client: TestClient) -> None: # Given: an agent with corrupted stored data - agent_id, _ = _init_agent(client) + agent_name, _ = _init_agent(client) with engine.begin() as conn: conn.execute( text("UPDATE agents SET data = CAST(:data AS JSONB) WHERE name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) # When: patching the agent resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_steps": [{"type": "tool", "name": "tool-a"}]}, ) @@ -841,15 +838,15 @@ def test_patch_agent_corrupted_data_returns_422(client: TestClient) -> None: def test_get_agent_evaluator_corrupted_data_returns_404(client: TestClient) -> None: # Given: an agent with evaluator data that becomes corrupted - agent_id, _ = _init_agent(client, evaluators=[{"name": "eval-a", "config_schema": {}}]) + agent_name, _ = _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 name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) # When: fetching a specific evaluator - resp = client.get(f"/api/v1/agents/{agent_id}/evaluators/eval-a") + resp = client.get(f"/api/v1/agents/{agent_name}/evaluators/eval-a") # Then: evaluator not found is returned due to corrupted data assert resp.status_code == 404 @@ -862,7 +859,7 @@ def test_init_agent_rejects_duplicate_step_names_in_single_request( # Given: a payload with duplicate step names payload = { "agent": { - "agent_id": str(uuid.uuid4()), + "agent_name": str(uuid.uuid4()), "agent_name": f"agent-{uuid.uuid4().hex[:12]}", "agent_description": "desc", "agent_version": "1.0", @@ -892,10 +889,9 @@ def test_init_agent_rejects_step_schema_conflict_across_registrations( ) -> None: # Given: an agent registered with a step agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name original_payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -916,7 +912,7 @@ def test_init_agent_rejects_step_schema_conflict_across_registrations( # When: re-registering with same step name but different schema conflicting_payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -946,10 +942,9 @@ def test_init_agent_accepts_identical_step_schema_across_registrations( ) -> None: # Given: an agent registered with a step agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "desc", "agent_version": "1.0", @@ -975,7 +970,7 @@ def test_init_agent_accepts_identical_step_schema_across_registrations( assert resp2.json()["created"] is False # Agent already exists # And: step is preserved - 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 steps = get_resp.json()["steps"] assert len(steps) == 1 diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 4be88a31..4743b6b5 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -499,11 +499,11 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ - "agent": {"agent_id": agent_id, "agent_name": agent_name}, + "agent": {"agent_name": agent_name, "agent_name": agent_name}, "steps": [], "evaluators": [], }, @@ -527,11 +527,11 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ - "agent": {"agent_id": agent_id, "agent_name": agent_name}, + "agent": {"agent_name": agent_name, "agent_name": agent_name}, "steps": [], "evaluators": [ { @@ -617,11 +617,11 @@ def test_set_control_data_agent_scoped_corrupted_agent_data_returns_422( ) -> None: # Given: an agent whose stored data is corrupted agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ - "agent": {"agent_id": agent_id, "agent_name": agent_name}, + "agent": {"agent_name": agent_name, "agent_name": agent_name}, "steps": [], "evaluators": [{"name": "custom", "config_schema": {"type": "object"}}], }, @@ -631,7 +631,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 name = :id"), - {"data": json.dumps({"bad": "data"}), "id": agent_id}, + {"data": json.dumps({"bad": "data"}), "id": agent_name}, ) control_id, _ = _create_control(client) diff --git a/server/tests/test_error_handling.py b/server/tests/test_error_handling.py index 756827c8..ce7cace3 100644 --- a/server/tests/test_error_handling.py +++ b/server/tests/test_error_handling.py @@ -32,10 +32,10 @@ def test_init_agent_rollback_on_create_failure( """Test that init_agent rolls back transaction when commit fails on create.""" # Given: a valid agent init payload agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "test", "agent_version": "1.0", @@ -63,7 +63,7 @@ def test_delete_agent_policy_rollback_on_failure( # Given: an agent with an assigned policy agent_payload = { "agent": { - "agent_id": str(uuid.uuid4()), + "agent_name": str(uuid.uuid4()), "agent_name": f"test-agent-{uuid.uuid4()}", "agent_description": "test", "agent_version": "1.0", @@ -73,14 +73,14 @@ 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_name"] + agent_name = agent_payload["agent"]["agent_name"] policy_name = f"test-policy-{uuid.uuid4()}" r2 = client.put("/api/v1/policies", json={"name": policy_name}) assert r2.status_code == 200 policy_id = r2.json()["policy_id"] - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign_resp.status_code == 200 # And: a database session that fails on commit @@ -89,7 +89,7 @@ def test_delete_agent_policy_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( - session.query(Agent).filter(Agent.name == agent_id).first() + session.query(Agent).filter(Agent.name == agent_name).first() ) assert existing_agent is not None @@ -106,7 +106,7 @@ async def mock_db_for_delete_policy() -> AsyncGenerator[AsyncSession, None]: # When: deleting policy and commit fails app.dependency_overrides[get_async_db] = mock_db_for_delete_policy try: - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_name}/policy") finally: app.dependency_overrides.clear() @@ -121,10 +121,10 @@ def test_init_agent_rollback_on_update_failure( """Test that init_agent rolls back transaction when commit fails on update.""" # Given: an existing agent agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "test", "agent_version": "1.0", @@ -221,10 +221,10 @@ def test_patch_agent_rollback_on_failure( """Test that patch_agent rolls back when commit fails.""" # Given: an existing agent with a step to remove agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name payload = { "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "test", "agent_version": "1.0", @@ -248,7 +248,7 @@ def test_patch_agent_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( - session.query(Agent).filter(Agent.name == agent_id).first() + session.query(Agent).filter(Agent.name == agent_name).first() ) assert existing_agent is not None @@ -266,7 +266,7 @@ async def mock_db_for_patch_agent() -> AsyncGenerator[AsyncSession, None]: app.dependency_overrides[get_async_db] = mock_db_for_patch_agent try: resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_steps": [{"type": "tool", "name": "tool_a"}]}, ) finally: @@ -349,7 +349,7 @@ def test_set_agent_policy_rollback_on_failure( # Given: an existing agent and policy agent_payload = { "agent": { - "agent_id": str(uuid.uuid4()), + "agent_name": str(uuid.uuid4()), "agent_name": f"test-agent-{uuid.uuid4()}", "agent_description": "test", "agent_version": "1.0", @@ -359,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_name"] + agent_name = agent_payload["agent"]["agent_name"] policy_name = f"test-policy-{uuid.uuid4()}" r2 = client.put("/api/v1/policies", json={"name": policy_name}) @@ -373,7 +373,7 @@ def test_set_agent_policy_rollback_on_failure( with Session(db_engine) as session: existing_agent = ( session.query(Agent) - .filter(Agent.name == agent_id) + .filter(Agent.name == agent_name) .first() ) existing_policy = ( @@ -416,7 +416,7 @@ async def mock_db_for_policy_assignment() -> AsyncGenerator[AsyncSession, None]: app.dependency_overrides[get_async_db] = mock_db_for_policy_assignment try: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: rollback is called and 500 error is returned assert resp.status_code == 500 diff --git a/server/tests/test_evaluator_schemas.py b/server/tests/test_evaluator_schemas.py index 785f41bf..bb952c06 100644 --- a/server/tests/test_evaluator_schemas.py +++ b/server/tests/test_evaluator_schemas.py @@ -9,13 +9,13 @@ def make_agent_payload( - agent_id: str | None = None, + agent_name: str | None = None, name: str | None = None, evaluators: list | None = None, ): """Helper to create agent payload with evaluators.""" - if agent_id is not None: - name = agent_id + if agent_name is not None: + name = agent_name elif name is None: name = f"agent-{uuid.uuid4().hex[:12]}" canonical_name = name.lower().replace(" ", "-") @@ -23,7 +23,7 @@ def make_agent_payload( canonical_name = f"{canonical_name}-agent".replace("--", "-") return { "agent": { - "agent_id": canonical_name, + "agent_name": canonical_name, "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", @@ -95,10 +95,10 @@ def test_init_agent_evaluator_name_collision_list(client: TestClient) -> None: def test_init_agent_update_evaluator_compatible_schema(client: TestClient) -> None: """Test updating evaluator with compatible schema change (add optional field).""" # Given: Agent with evaluator - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -116,7 +116,7 @@ def test_init_agent_update_evaluator_compatible_schema(client: TestClient) -> No # When: Updating with compatible schema (add optional field) payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -142,10 +142,10 @@ def test_init_agent_update_evaluator_incompatible_schema_rejected( ) -> None: """Test that incompatible schema change is rejected.""" # Given: Agent with evaluator - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -162,7 +162,7 @@ def test_init_agent_update_evaluator_incompatible_schema_rejected( # When: Updating with incompatible schema (remove property) payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -183,10 +183,10 @@ def test_init_agent_update_evaluator_incompatible_schema_rejected( def test_init_agent_update_evaluator_type_change_rejected(client: TestClient) -> None: """Test that changing property type is rejected.""" # Given: Agent with evaluator - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -202,7 +202,7 @@ def test_init_agent_update_evaluator_type_change_rejected(client: TestClient) -> # When: Changing property type payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -223,10 +223,10 @@ def test_init_agent_update_evaluator_type_change_rejected(client: TestClient) -> def test_init_agent_add_required_property_rejected(client: TestClient) -> None: """Test that adding a new required property is rejected.""" # Given: Agent with evaluator - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -242,7 +242,7 @@ def test_init_agent_add_required_property_rejected(client: TestClient) -> None: # When: Adding new required property payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -280,10 +280,10 @@ def test_list_agent_evaluators(client: TestClient) -> None: ) resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: Listing evaluators - list_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators") + list_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators") # Then: Should return both evaluators assert list_resp.status_code == 200 data = list_resp.json() @@ -301,10 +301,10 @@ def test_list_agent_evaluators_pagination(client: TestClient) -> None: ) resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: Fetching first page - resp1 = client.get(f"/api/v1/agents/{agent_id}/evaluators?offset=0&limit=2") + resp1 = client.get(f"/api/v1/agents/{agent_name}/evaluators?offset=0&limit=2") # Then: Should return 2 items with total=5 assert resp1.status_code == 200 data1 = resp1.json() @@ -312,7 +312,7 @@ def test_list_agent_evaluators_pagination(client: TestClient) -> None: assert data1["pagination"]["total"] == 5 # When: Fetching second page - resp2 = client.get(f"/api/v1/agents/{agent_id}/evaluators?offset=2&limit=2") + resp2 = client.get(f"/api/v1/agents/{agent_name}/evaluators?offset=2&limit=2") # Then: Should return 2 more items assert resp2.status_code == 200 data2 = resp2.json() @@ -333,10 +333,10 @@ def test_get_agent_evaluator_by_name(client: TestClient) -> None: ) resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: Getting evaluator by name - get_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators/my-eval") + get_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators/my-eval") # Then: Should return evaluator details assert get_resp.status_code == 200 data = get_resp.json() @@ -350,10 +350,10 @@ def test_get_agent_evaluator_not_found(client: TestClient) -> None: payload = make_agent_payload(evaluators=[]) resp = client.post("/api/v1/agents/initAgent", json=payload) assert resp.status_code == 200 - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: Getting nonexistent evaluator - get_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators/nonexistent") + get_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators/nonexistent") # Then: Should return 404 assert get_resp.status_code == 404 diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index 2bc46e5e..e67a0988 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -17,11 +17,11 @@ def make_agent_payload( - agent_id: str | None = None, + agent_name: str | None = None, name: str = "testagent0001", steps: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: - resolved_name = name if name != "testagent0001" else (agent_id or f"agent-{uuid.uuid4().hex[:12]}") + resolved_name = name if name != "testagent0001" else (agent_name 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("--", "-") @@ -36,7 +36,7 @@ def make_agent_payload( ] return { "agent": { - "agent_id": canonical_name, + "agent_name": canonical_name, "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", @@ -120,7 +120,7 @@ def test_agent_endpoints_normalize_mixed_case_agent_name(client: TestClient) -> assert delete_policy_resp.status_code == 200 -def test_init_agent_idempotent_same_steps(client: TestClient) -> None: +def test_init_agent_nameempotent_same_steps(client: TestClient) -> None: # Given: an init payload payload = make_agent_payload() # When: initializing the agent the first time @@ -183,8 +183,8 @@ def test_init_agent_updates_metadata_on_reinit(client: TestClient) -> None: def test_init_agent_adds_new_step(client: TestClient) -> None: # Given: an agent id and base payload - agent_id = str(uuid.uuid4()) - base = make_agent_payload(agent_id=agent_id) + agent_name = str(uuid.uuid4()) + base = make_agent_payload(agent_name=agent_name) # When: initializing the agent r1 = client.post("/api/v1/agents/initAgent", json=base) assert r1.status_code == 200 @@ -200,14 +200,14 @@ def test_init_agent_adds_new_step(client: TestClient) -> None: ] r2 = client.post( "/api/v1/agents/initAgent", - json=make_agent_payload(agent_id=agent_id, steps=steps), + json=make_agent_payload(agent_name=agent_name, steps=steps), ) assert r2.status_code == 200 # Then: the agent is not newly created assert r2.json()["created"] is False # When: fetching the agent - g = client.get(f"/api/v1/agents/{agent_id}") + g = client.get(f"/api/v1/agents/{agent_name}") assert g.status_code == 200 names = {s["name"] for s in g.json()["steps"]} # Then: both steps are present @@ -216,15 +216,15 @@ def test_init_agent_adds_new_step(client: TestClient) -> None: def test_init_agent_overwrites_step_on_signature_change(client: TestClient) -> None: # Given: a base payload for an agent - agent_id = str(uuid.uuid4()) - base = make_agent_payload(agent_id=agent_id) + agent_name = str(uuid.uuid4()) + base = make_agent_payload(agent_name=agent_name) # When: initializing the agent r1 = client.post("/api/v1/agents/initAgent", json=base) assert r1.status_code == 200 # When: updating tool_a schema changed = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, steps=[ { "type": "tool", @@ -245,8 +245,8 @@ def test_init_agent_overwrites_step_on_signature_change(client: TestClient) -> N def test_get_agent_returns_evaluators(client: TestClient) -> None: """Test that GET /agents/{id} returns evaluators.""" # Given: an agent with evaluators - agent_id = str(uuid.uuid4()) - payload = make_agent_payload(agent_id=agent_id) + agent_name = str(uuid.uuid4()) + payload = make_agent_payload(agent_name=agent_name) payload["evaluators"] = [ {"name": "eval-a", "description": "First", "config_schema": {}}, {"name": "eval-b", "description": "Second", "config_schema": {"type": "object"}}, @@ -255,7 +255,7 @@ def test_get_agent_returns_evaluators(client: TestClient) -> None: assert resp.status_code == 200 # When: fetching the agent - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") # Then: evaluators are included in the response assert get_resp.status_code == 200 data = get_resp.json() @@ -325,10 +325,10 @@ def test_set_agent_policy_first_time(client: TestClient) -> None: payload = make_agent_payload() r = client.post("/api/v1/agents/initAgent", json=payload) assert r.status_code == 200 - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: assigning policy the first time - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: success and no old policy assert resp.status_code == 200 body = resp.json() @@ -341,11 +341,11 @@ def test_get_agent_policy_after_assignment(client: TestClient) -> None: policy_id = _create_policy(client) payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + agent_name = payload["agent"]["agent_name"] + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # When: retrieving policy - resp = client.get(f"/api/v1/agents/{agent_id}/policy") + resp = client.get(f"/api/v1/agents/{agent_name}/policy") # Then: we see the assigned policy id assert resp.status_code == 200 assert resp.json()["policy_id"] == policy_id @@ -357,11 +357,11 @@ def test_reassign_agent_policy_returns_old_id(client: TestClient) -> None: second = _create_policy(client) payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{first}") + agent_name = payload["agent"]["agent_name"] + client.post(f"/api/v1/agents/{agent_name}/policy/{first}") # When: reassigning to another policy - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{second}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{second}") # Then: success and old_policy_id equals the first policy id assert resp.status_code == 200 assert resp.json()["success"] is True @@ -373,17 +373,17 @@ def test_delete_agent_policy_then_get_404(client: TestClient) -> None: policy_id = _create_policy(client) payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + agent_name = payload["agent"]["agent_name"] + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # When: removing the policy association - del_resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + del_resp = client.delete(f"/api/v1/agents/{agent_name}/policy") # Then: deletion success assert del_resp.status_code == 200 assert del_resp.json()["success"] is True # When: fetching policy after deletion - get_resp = client.get(f"/api/v1/agents/{agent_id}/policy") + get_resp = client.get(f"/api/v1/agents/{agent_name}/policy") # Then: not found assert get_resp.status_code == 404 @@ -403,11 +403,11 @@ def test_set_policy_not_found_returns_404(client: TestClient) -> None: # Given: an agent and a bogus policy id payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] bogus_policy = "999999999" # When: assigning a non-existent policy - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{bogus_policy}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{bogus_policy}") # Then: 404 assert resp.status_code == 404 @@ -416,10 +416,10 @@ def test_list_agent_controls_no_policy_returns_empty(client: TestClient) -> None # Given: an agent without a policy payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # When: listing controls - r = client.get(f"/api/v1/agents/{agent_id}/controls") + r = client.get(f"/api/v1/agents/{agent_name}/controls") # Then: empty list assert r.status_code == 200 assert r.json()["controls"] == [] @@ -429,7 +429,7 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: # Given: an agent with a policy containing one control set and one control payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] # Create policy, control, and wire them pol_name = f"pol-{uuid.uuid4()}" @@ -447,10 +447,10 @@ def test_list_agent_controls_with_policy(client: TestClient) -> None: # Associate control -> policy; assign policy to agent client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # When: listing controls - r = client.get(f"/api/v1/agents/{agent_id}/controls") + r = client.get(f"/api/v1/agents/{agent_name}/controls") # Then: contains our control serialized via API model assert r.status_code == 200 body = r.json() @@ -556,10 +556,10 @@ def test_list_agents_with_policy(client: TestClient) -> None: # Given: an agent with a policy assigned payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) - agent_id = payload["agent"]["agent_id"] + agent_name = payload["agent"]["agent_name"] policy_id = _create_policy(client) - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # When: listing agents resp = client.get("/api/v1/agents") @@ -573,11 +573,11 @@ def test_list_agents_with_policy(client: TestClient) -> None: def test_list_agents_pagination(client: TestClient) -> None: """Test cursor-based pagination works correctly.""" # Given: 5 agents - agent_ids = [] + agent_names = [] for i in range(5): - agent_id = str(uuid.uuid4()) - agent_ids.append(agent_id) - payload = make_agent_payload(agent_id=agent_id, name=f"Agent {i}") + agent_name = str(uuid.uuid4()) + agent_names.append(agent_name) + payload = make_agent_payload(agent_name=agent_name, name=f"Agent {i}") r = client.post("/api/v1/agents/initAgent", json=payload) assert r.status_code == 200 diff --git a/server/tests/test_init_agent_conflict_mode.py b/server/tests/test_init_agent_conflict_mode.py index 0cd1b714..e39f1c02 100644 --- a/server/tests/test_init_agent_conflict_mode.py +++ b/server/tests/test_init_agent_conflict_mode.py @@ -13,7 +13,6 @@ def _init_payload( *, - agent_id: str, agent_name: str, agent_description: str = "desc", agent_version: str = "1.0", @@ -24,7 +23,6 @@ def _init_payload( canonical_name = agent_name.lower() payload: dict[str, Any] = { "agent": { - "agent_id": canonical_name, "agent_name": canonical_name, "agent_description": agent_description, "agent_version": agent_version, @@ -73,10 +71,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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name create_payload = _init_payload( - agent_id=agent_id, agent_name=agent_name, steps=[ { @@ -104,7 +100,6 @@ def test_init_agent_overwrite_replaces_steps_and_evaluators(client: TestClient) # When: initAgent is called in overwrite mode with an updated registration payload. overwrite_payload = _init_payload( - agent_id=agent_id, agent_name=agent_name, agent_description="updated desc", agent_version="2.0", @@ -154,7 +149,7 @@ def test_init_agent_overwrite_replaces_steps_and_evaluators(client: TestClient) } ] - 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 get_data = get_resp.json() assert get_data["agent"]["agent_description"] == "updated desc" @@ -165,13 +160,11 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name evaluator_name = "custom-eval" init_resp = client.post( "/api/v1/agents/initAgent", json=_init_payload( - agent_id=agent_id, agent_name=agent_name, evaluators=[{"name": evaluator_name, "config_schema": {"type": "object"}}], ), @@ -181,14 +174,13 @@ def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: Test policy_id, control_id, control_name = _create_policy_with_agent_evaluator_control( client, agent_name=agent_name, evaluator_name=evaluator_name ) - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign_resp.status_code == 200 # When: overwrite mode removes the evaluator from the incoming registration payload. overwrite_resp = client.post( "/api/v1/agents/initAgent", json=_init_payload( - agent_id=agent_id, agent_name=agent_name, evaluators=[], conflict_mode="overwrite", @@ -209,7 +201,7 @@ def test_init_agent_overwrite_warns_on_removed_referenced_evaluator(client: Test } ] - get_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators") + get_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators") assert get_resp.status_code == 200 assert get_resp.json()["evaluators"] == [] @@ -217,9 +209,7 @@ 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_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name payload = _init_payload( - agent_id=agent_id, agent_name=agent_name, steps=[{"type": "tool", "name": "tool-a", "input_schema": {}, "output_schema": {}}], evaluators=[{"name": "eval-a", "description": "x", "config_schema": {"type": "object"}}], diff --git a/server/tests/test_init_agent_force_replace.py b/server/tests/test_init_agent_force_replace.py index f324a641..9a3e75a4 100644 --- a/server/tests/test_init_agent_force_replace.py +++ b/server/tests/test_init_agent_force_replace.py @@ -19,12 +19,12 @@ def test_init_agent_force_replace_default_false_works_normally(client: TestClien """ # Given: New agent agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name # When: Create without force_replace (default) resp = client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Test", "agent_version": "1.0" @@ -46,12 +46,12 @@ def test_init_agent_force_replace_false_explicit_works_normally(client: TestClie """ # Given: New agent agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name # When: Create with force_replace=false resp = client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Test", "agent_version": "1.0" @@ -74,11 +74,11 @@ def test_init_agent_force_replace_true_on_valid_data_works_normally(client: Test """ # Given: Create agent with steps agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name resp = client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Test", "agent_version": "1.0" @@ -93,7 +93,7 @@ def test_init_agent_force_replace_true_on_valid_data_works_normally(client: Test # When: Update with force_replace=true and add a new step resp = client.post("/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Updated", "agent_version": "2.0" @@ -108,7 +108,7 @@ def test_init_agent_force_replace_true_on_valid_data_works_normally(client: Test # Then: Should succeed and all steps should be present assert resp.status_code == 200 - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") steps = [s["name"] for s in get_resp.json()["steps"]] assert set(steps) == {"tool1", "tool2", "tool3"} @@ -131,12 +131,12 @@ def test_init_agent_force_replace_recovers_from_corrupted_data(client: TestClien """Test that force_replace=true replaces corrupted stored data.""" # Given: an existing agent with corrupted data in the DB agent_name = f"agent-{uuid.uuid4().hex[:12]}" - agent_id = agent_name + agent_name = agent_name resp = client.post( "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Test", "agent_version": "1.0", @@ -156,7 +156,7 @@ def test_init_agent_force_replace_recovers_from_corrupted_data(client: TestClien "/api/v1/agents/initAgent", json={ "agent": { - "agent_id": agent_id, + "agent_name": agent_name, "agent_name": agent_name, "agent_description": "Replaced", "agent_version": "2.0", @@ -170,7 +170,7 @@ def test_init_agent_force_replace_recovers_from_corrupted_data(client: TestClien # Then: request succeeds and stored data is replaced assert resp.status_code == 200 - 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 data = get_resp.json() assert data["agent"]["agent_description"] == "Replaced" diff --git a/server/tests/test_new_features.py b/server/tests/test_new_features.py index 01bc96ec..ad5a91c8 100644 --- a/server/tests/test_new_features.py +++ b/server/tests/test_new_features.py @@ -6,14 +6,14 @@ def make_agent_payload( - agent_id: str | None = None, + agent_name: str | None = None, name: str | None = None, steps: list | None = None, evaluators: list | None = None, ): """Helper to create agent payload.""" - if agent_id is not None: - name = agent_id + if agent_name is not None: + name = agent_name elif name is None: name = f"agent-{uuid.uuid4().hex[:12]}" canonical_name = name.lower().replace(" ", "-") @@ -21,7 +21,7 @@ def make_agent_payload( canonical_name = f"{canonical_name}-agent".replace("--", "-") return { "agent": { - "agent_id": canonical_name, + "agent_name": canonical_name, "agent_name": canonical_name, "agent_description": "desc", "agent_version": "1.0", @@ -79,10 +79,10 @@ def test_get_evaluators_schema_has_properties(client: TestClient) -> None: def test_patch_agent_remove_step(client: TestClient) -> None: """Given an agent with multiple steps, when removing one step, then only that step is removed.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, steps=[ {"type": "tool", "name": "tool1", "input_schema": {}, "output_schema": {}}, @@ -93,7 +93,7 @@ def test_patch_agent_remove_step(client: TestClient) -> None: # When: patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_steps": [{"type": "tool", "name": "tool1"}]}, ) @@ -103,7 +103,7 @@ def test_patch_agent_remove_step(client: TestClient) -> None: assert data["steps_removed"] == [{"type": "tool", "name": "tool1"}] assert data["evaluators_removed"] == [] - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") steps = [s["name"] for s in get_resp.json()["steps"]] assert "tool1" not in steps assert "tool2" in steps @@ -112,10 +112,10 @@ def test_patch_agent_remove_step(client: TestClient) -> None: def test_patch_agent_remove_evaluator(client: TestClient) -> None: """Given an agent with multiple evaluators, when removing one, then only that evaluator is removed.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ {"name": "eval1", "config_schema": {}}, @@ -126,7 +126,7 @@ def test_patch_agent_remove_evaluator(client: TestClient) -> None: # When: patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_evaluators": ["eval1"]}, ) @@ -135,7 +135,7 @@ def test_patch_agent_remove_evaluator(client: TestClient) -> None: data = patch_resp.json() assert data["evaluators_removed"] == ["eval1"] - get_resp = client.get(f"/api/v1/agents/{agent_id}/evaluators") + get_resp = client.get(f"/api/v1/agents/{agent_name}/evaluators") evals = [e["name"] for e in get_resp.json()["evaluators"]] assert "eval1" not in evals assert "eval2" in evals @@ -144,14 +144,14 @@ def test_patch_agent_remove_evaluator(client: TestClient) -> None: def test_patch_agent_remove_nonexistent_is_idempotent(client: TestClient) -> None: """Given an agent, when removing nonexistent items, then succeeds with empty removed lists.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" - payload = make_agent_payload(agent_id=agent_id, name=name) + payload = make_agent_payload(agent_name=agent_name, name=name) client.post("/api/v1/agents/initAgent", json=payload) # When: patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={ "remove_steps": [{"type": "tool", "name": "nonexistent"}], "remove_evaluators": ["also_nonexistent"], @@ -183,10 +183,10 @@ def test_patch_agent_not_found(client: TestClient) -> None: def test_patch_agent_remove_both(client: TestClient) -> None: """Given an agent with steps and evaluators, when removing both, then both are removed.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, steps=[{"type": "tool", "name": "my_tool", "input_schema": {}, "output_schema": {}}], evaluators=[{"name": "my_eval", "config_schema": {}}], @@ -195,7 +195,7 @@ def test_patch_agent_remove_both(client: TestClient) -> None: # When: patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={ "remove_steps": [{"type": "tool", "name": "my_tool"}], "remove_evaluators": ["my_eval"], @@ -212,10 +212,10 @@ def test_patch_agent_remove_both(client: TestClient) -> None: def test_patch_agent_empty_request_is_noop(client: TestClient) -> None: """Given an agent, when patching with empty lists, then nothing changes and succeeds.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, steps=[{"type": "tool", "name": "keep_me", "input_schema": {}, "output_schema": {}}], evaluators=[{"name": "keep_me_too", "config_schema": {}}], @@ -224,7 +224,7 @@ def test_patch_agent_empty_request_is_noop(client: TestClient) -> None: # When: patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_steps": [], "remove_evaluators": []}, ) @@ -235,11 +235,11 @@ def test_patch_agent_empty_request_is_noop(client: TestClient) -> None: assert data["evaluators_removed"] == [] # Verify nothing was removed - get_resp = client.get(f"/api/v1/agents/{agent_id}") + get_resp = client.get(f"/api/v1/agents/{agent_name}") steps = [s["name"] for s in get_resp.json()["steps"]] assert "keep_me" in steps - get_evals = client.get(f"/api/v1/agents/{agent_id}/evaluators") + get_evals = client.get(f"/api/v1/agents/{agent_name}/evaluators") evals = [e["name"] for e in get_evals.json()["evaluators"]] assert "keep_me_too" in evals @@ -282,9 +282,9 @@ def _create_policy_with_control( def test_policy_assignment_with_builtin_evaluator(client: TestClient) -> None: """Given an agent and a policy with built-in evaluator control, when assigning policy, then succeeds.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" - payload = make_agent_payload(agent_id=agent_id, name=name) + payload = make_agent_payload(agent_name=agent_name, name=name) client.post("/api/v1/agents/initAgent", json=payload) policy_id, _ = _create_policy_with_control( @@ -301,7 +301,7 @@ def test_policy_assignment_with_builtin_evaluator(client: TestClient) -> None: ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: assert resp.status_code == 200 @@ -310,10 +310,10 @@ 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 = f"agent-{uuid.uuid4().hex[:12]}" - agent_name = agent_id + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_name payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=agent_name, evaluators=[{"name": "custom-eval", "config_schema": {"type": "object"}}], ) @@ -333,7 +333,7 @@ def test_policy_assignment_with_registered_agent_evaluator(client: TestClient) - ) # When: - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Then: assert resp.status_code == 200 @@ -342,9 +342,9 @@ 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 = f"agent-{uuid.uuid4().hex[:12]}" - agent_name = agent_id - payload = make_agent_payload(agent_id=agent_id, name=agent_name) + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_name + payload = make_agent_payload(agent_name=agent_name, name=agent_name) client.post("/api/v1/agents/initAgent", json=payload) ctl_resp = client.put("/api/v1/controls", json={"name": f"control-{uuid.uuid4().hex[:8]}"}) @@ -377,7 +377,7 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No 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, + agent_name=agent_a_id, name=agent_a_name, evaluators=[{"name": "shared-eval", "config_schema": {"type": "object"}}], ) @@ -385,7 +385,7 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No 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) + payload_b = make_agent_payload(agent_name=agent_b_id, name=agent_b_name) client.post("/api/v1/agents/initAgent", json=payload_b) policy_id, _ = _create_policy_with_control( @@ -426,10 +426,10 @@ def test_policy_assignment_cross_agent_evaluator_fails(client: TestClient) -> No def test_schema_compat_nested_additional_properties_compatible(client: TestClient) -> None: """Given a nested schema, when adding optional property in nested object, then compatible.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -450,7 +450,7 @@ def test_schema_compat_nested_additional_properties_compatible(client: TestClien # When: add optional property in nested object payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -479,10 +479,10 @@ def test_schema_compat_nested_additional_properties_compatible(client: TestClien def test_schema_compat_nested_type_change_incompatible(client: TestClient) -> None: """Given a nested schema, when changing nested property type, then rejected as incompatible.""" # Given: - agent_id = str(uuid.uuid4()) + agent_name = str(uuid.uuid4()) name = f"Test Agent {uuid.uuid4().hex[:8]}" payload1 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -503,7 +503,7 @@ def test_schema_compat_nested_type_change_incompatible(client: TestClient) -> No # When: change nested type from integer to string payload2 = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=name, evaluators=[ { @@ -540,10 +540,10 @@ 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 = f"agent-{uuid.uuid4().hex[:12]}" - agent_name = agent_id + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_name payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=agent_name, evaluators=[{"name": "my-eval", "config_schema": {"type": "object"}}], ) @@ -564,12 +564,12 @@ def test_patch_agent_remove_evaluator_blocked_by_control(client: TestClient) -> ) # And: Policy assigned to agent - assign_resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + assign_resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert assign_resp.status_code == 200 # When: Trying to remove the evaluator patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_evaluators": ["my-eval"]}, ) @@ -591,10 +591,10 @@ 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 = f"agent-{uuid.uuid4().hex[:12]}" - agent_name = agent_id + agent_name = f"agent-{uuid.uuid4().hex[:12]}" + agent_name = agent_name payload = make_agent_payload( - agent_id=agent_id, + agent_name=agent_name, name=agent_name, evaluators=[{"name": "my-eval", "config_schema": {"type": "object"}}], ) @@ -602,7 +602,7 @@ def test_patch_agent_remove_evaluator_allowed_without_policy(client: TestClient) # When: Removing the evaluator (no policy = no controls can reference it) patch_resp = client.patch( - f"/api/v1/agents/{agent_id}", + f"/api/v1/agents/{agent_name}", json={"remove_evaluators": ["my-eval"]}, ) diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index 446c520a..129c2528 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -55,7 +55,7 @@ def _create_control(client: TestClient, name: str | None = None, data: dict | No def test_agent_gets_controls_from_policy(client: TestClient) -> None: """Agent should see all controls from its policy.""" # Given: Agent with policy containing 6 controls - agent_id, _ = _create_agent(client) + agent_name, _ = _create_agent(client) policy_id = _create_policy(client) # Create 6 controls @@ -69,11 +69,11 @@ def test_agent_gets_controls_from_policy(client: TestClient) -> None: assert resp.status_code == 200 # Assign policy to agent - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") assert resp.status_code == 200 # When: Get agent's controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") assert resp.status_code == 200 controls = resp.json()["controls"] @@ -88,7 +88,7 @@ def test_agent_gets_controls_from_policy(client: TestClient) -> None: def test_agent_controls_update_when_control_added_to_policy(client: TestClient) -> None: """Adding a control to policy should make it visible to agents.""" # Given: Agent → Policy → 2 controls - agent_id, _ = _create_agent(client) + agent_name, _ = _create_agent(client) policy_id = _create_policy(client) control_1_id = _create_control(client, "control-1", {"id": 1}) @@ -96,10 +96,10 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) client.post(f"/api/v1/policies/{policy_id}/controls/{control_1_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Verify initial state: 2 controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") assert len(resp.json()["controls"]) == 2 # When: Add 3 more controls to the policy @@ -115,7 +115,7 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) assert resp.status_code == 200 # Then: Agent now sees 5 controls total - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") controls = resp.json()["controls"] assert len(controls) == 5 @@ -126,7 +126,7 @@ def test_agent_controls_update_when_control_added_to_policy(client: TestClient) def test_switching_agent_policy_changes_controls(client: TestClient) -> None: """Switching agent's policy should completely change its controls.""" # Given: Two policies with different controls - agent_id, _ = _create_agent(client) + agent_name, _ = _create_agent(client) # Policy A with controls {1, 2} policy_a_id = _create_policy(client, "policy-a") @@ -143,18 +143,18 @@ def test_switching_agent_policy_changes_controls(client: TestClient) -> None: client.post(f"/api/v1/policies/{policy_b_id}/controls/{control_4_id}") # Assign policy A to agent - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_a_id}") - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_a_id}") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") controls_a = resp.json()["controls"] assert len(controls_a) == 2 assert {r["id"] for r in controls_a} == {control_1_id, control_2_id} # When: Switch to policy B - resp = client.post(f"/api/v1/agents/{agent_id}/policy/{policy_b_id}") + resp = client.post(f"/api/v1/agents/{agent_name}/policy/{policy_b_id}") assert resp.status_code == 200 # Then: Agent's controls change completely - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") controls_b = resp.json()["controls"] assert len(controls_b) == 2 assert {r["id"] for r in controls_b} == {control_3_id, control_4_id} @@ -163,23 +163,23 @@ def test_switching_agent_policy_changes_controls(client: TestClient) -> None: def test_removing_agent_policy_clears_controls(client: TestClient) -> None: """Removing policy from agent should result in empty controls list.""" # Given: Agent with policy that has controls - agent_id, _ = _create_agent(client) + agent_name, _ = _create_agent(client) policy_id = _create_policy(client) control_id = _create_control(client, "control-1", {"id": 1}) client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Verify agent has controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") assert len(resp.json()["controls"]) > 0 # When: Remove policy from agent - resp = client.delete(f"/api/v1/agents/{agent_id}/policy") + resp = client.delete(f"/api/v1/agents/{agent_name}/policy") assert resp.status_code == 200 # Then: Agent returns empty controls list - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") assert resp.status_code == 200 assert resp.json()["controls"] == [] @@ -187,7 +187,7 @@ def test_removing_agent_policy_clears_controls(client: TestClient) -> None: def test_removing_control_from_policy_removes_from_agent(client: TestClient) -> None: """Removing control from policy should remove it from agent.""" # Given: Agent → Policy → 4 controls - agent_id, _ = _create_agent(client) + agent_name, _ = _create_agent(client) policy_id = _create_policy(client) control_1_id = _create_control(client, "control-1", {"id": 1}) @@ -199,10 +199,10 @@ def test_removing_control_from_policy_removes_from_agent(client: TestClient) -> client.post(f"/api/v1/policies/{policy_id}/controls/{control_2_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_3_id}") client.post(f"/api/v1/policies/{policy_id}/controls/{control_4_id}") - client.post(f"/api/v1/agents/{agent_id}/policy/{policy_id}") + client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") # Verify initial state: 4 controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") assert len(resp.json()["controls"]) == 4 # When: Remove 2 controls from policy @@ -212,7 +212,7 @@ def test_removing_control_from_policy_removes_from_agent(client: TestClient) -> assert resp.status_code == 200 # Then: Agent sees 2 remaining controls - resp = client.get(f"/api/v1/agents/{agent_id}/controls") + resp = client.get(f"/api/v1/agents/{agent_name}/controls") controls = resp.json()["controls"] assert len(controls) == 2 assert {r["id"] for r in controls} == {control_3_id, control_4_id} diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index d60c0705..4eb61066 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -108,7 +108,7 @@ export type 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; agent_name: string; }; @@ -134,10 +134,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..069387ea 100644 --- a/ui/src/core/hooks/query-hooks/use-agent-controls.ts +++ b/ui/src/core/hooks/query-hooks/use-agent-controls.ts @@ -12,7 +12,7 @@ import type { * @param agentId - UUID of the agent (required) */ export function useAgentControls( - agentId: GetAgentControlsPathParams['agent_id'] + agentId: GetAgentControlsPathParams['agent_name'] ) { return useQuery({ queryKey: ['agent', agentId, 'controls'], diff --git a/ui/src/core/hooks/query-hooks/use-agent.ts b/ui/src/core/hooks/query-hooks/use-agent.ts index 6c728ca4..ee5bb656 100644 --- a/ui/src/core/hooks/query-hooks/use-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-agent.ts @@ -11,7 +11,7 @@ import type { GetAgentPathParams, GetAgentResponse } from '@/core/api/types'; * */ export function useAgent( - agentId: GetAgentPathParams['agent_id'], + agentId: GetAgentPathParams['agent_name'], options?: Omit< UseQueryOptions< GetAgentResponse, 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 d0bc9c6d..ecbee7e3 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/home/home.tsx b/ui/src/core/page-components/home/home.tsx index 458a807a..8b57fd80 100644 --- a/ui/src/core/page-components/home/home.tsx +++ b/ui/src/core/page-components/home/home.tsx @@ -45,7 +45,7 @@ from agent_control import control, ControlViolationError agent_control.init( agent_name="Customer Support Agent", - agent_id="support-agent-v1", + agent_name="support-agent-v1", server_url="http://localhost:8000", ) @@ -102,7 +102,7 @@ const HomePage = () => { }, [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/home.spec.ts b/ui/tests/home.spec.ts index d2a95886..f1c75adc 100644 --- a/ui/tests/home.spec.ts +++ b/ui/tests/home.spec.ts @@ -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` ); }); From 4f75200ccc4982c47cf1788c538f8c6eb931edc0 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 15:05:34 -0800 Subject: [PATCH 21/32] fix(ui/examples): align agent control flows with policy-based APIs --- README.md | 6 +- docs/REFERENCE.md | 4 +- examples/README.md | 4 +- examples/agent_control_demo/demo_agent.py | 3 +- examples/crewai/content_agent_protection.py | 1 - examples/customer_support_agent/README.md | 1 - .../customer_support_agent/support_agent.py | 1 - examples/deepeval/qa_agent.py | 3 +- .../langchain/langgraph_auto_schema_agent.py | 1 - sdks/python/README.md | 27 +++--- ui/src/core/api/client.ts | 4 + ui/src/core/api/types.ts | 1 - .../query-hooks/use-add-control-to-agent.ts | 86 ++++++++++++++++++- .../hooks/query-hooks/use-delete-control.ts | 33 +++++-- .../controls/use-delete-control-flow.tsx | 21 ++--- .../modals/control-store/index.tsx | 6 +- ui/src/core/page-components/home/home.tsx | 2 +- 17 files changed, 150 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index bd6a6c5f..4268c144 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ async def setup(): agent = Agent( # Your agent's UUID agent_name="550e8400-e29b-41d4-a716-446655440000", - agent_name="My Chatbot", + agent_description="My Chatbot", agent_created_at=datetime.now(UTC).isoformat() ) await agents.register_agent(client, agent, steps=[]) @@ -235,8 +235,8 @@ from agent_control import control, ControlViolationError # Initialize your agent agent_control.init( - agent_name="My Chatbot", - agent_name="550e8400-e29b-41d4-a716-446655440000" + agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="My Chatbot", ) # Protect any function (like LLM calls) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 2d89706c..66e8c2e7 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -586,8 +586,8 @@ The Python SDK provides decorator-based protection and programmatic control mana import agent_control agent_control.init( - agent_name="my-agent", # Required: human-readable name - agent_name="550e8400-e29b-41d4-a716-446655440000", # Required: UUID + agent_name="my-agent", # Required: unique identifier + agent_description="My Agent", # Optional: human-readable description server_url="http://localhost:8000", # Optional: defaults to env var policy_refresh_interval_seconds=60, # Optional: set 0 to disable background refresh steps=[ # Optional: register available steps diff --git a/examples/README.md b/examples/README.md index e5c2dd9c..cb3ccf3f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -129,7 +129,7 @@ from agent_control import control, ControlViolationError # Initialize agent (connects to server, loads policy) agent_control.init( agent_name="my-bot", - agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="My Bot", ) # Apply the agent's assigned policy @@ -150,7 +150,7 @@ except ControlViolationError as e: import agent_control from agent_control import control, ControlSteerError, ControlViolationError -agent_control.init(agent_name="my-bot", agent_name="...") +agent_control.init(agent_name="my-bot", agent_description="My Bot") @control() async def generate_content(prompt: str) -> str: diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 4e033bb0..14f37aab 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -161,10 +161,9 @@ async def run_demo(): try: logger.info(f"Initializing agent: {AGENT_NAME}") agent_control.init( - agent_name=AGENT_NAME, agent_name=AGENT_ID, + agent_description=AGENT_NAME, server_url=SERVER_URL, - agent_description="Demo chatbot for testing controls" ) logger.info("Agent initialized successfully") except Exception as e: diff --git a/examples/crewai/content_agent_protection.py b/examples/crewai/content_agent_protection.py index ecef00f1..2827d4e3 100644 --- a/examples/crewai/content_agent_protection.py +++ b/examples/crewai/content_agent_protection.py @@ -53,7 +53,6 @@ server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") agent_control.init( - agent_name=AGENT_NAME, agent_name=AGENT_ID, agent_description=AGENT_DESCRIPTION, server_url=server_url, diff --git a/examples/customer_support_agent/README.md b/examples/customer_support_agent/README.md index 30571390..99d5d475 100644 --- a/examples/customer_support_agent/README.md +++ b/examples/customer_support_agent/README.md @@ -160,7 +160,6 @@ Initialize once at application startup: import agent_control agent_control.init( - agent_name="Customer Support Agent", agent_name="646d5dea-c2e6-4453-b446-7035482b38e4", agent_description="AI-powered customer support assistant", ) diff --git a/examples/customer_support_agent/support_agent.py b/examples/customer_support_agent/support_agent.py index 821890d9..82273005 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -29,7 +29,6 @@ # The agent registers with the server and loads its assigned policy. agent_control.init( - agent_name="Customer Support Agent", 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.", diff --git a/examples/deepeval/qa_agent.py b/examples/deepeval/qa_agent.py index 90cfd4a8..91bf4bda 100755 --- a/examples/deepeval/qa_agent.py +++ b/examples/deepeval/qa_agent.py @@ -37,9 +37,8 @@ # ============================================================================= agent_control.init( - agent_name="Q&A Agent with DeepEval", agent_name="qa-agent-deepeval", - agent_description="Question answering agent with DeepEval quality controls", + agent_description="Q&A Agent with DeepEval", agent_version="1.0.0", ) diff --git a/examples/langchain/langgraph_auto_schema_agent.py b/examples/langchain/langgraph_auto_schema_agent.py index ac9acd11..b077d0df 100644 --- a/examples/langchain/langgraph_auto_schema_agent.py +++ b/examples/langchain/langgraph_auto_schema_agent.py @@ -241,7 +241,6 @@ 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_name=AGENT_ID, agent_description=AGENT_DESCRIPTION, server_url=os.getenv("AGENT_CONTROL_URL"), diff --git a/sdks/python/README.md b/sdks/python/README.md index e7811d4d..804394c8 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -17,8 +17,8 @@ import agent_control # Initialize at the base of your agent agent_control.init( - agent_name="My Customer Service Bot", - agent_name="550e8400-e29b-41d4-a716-446655440000" + agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="My Customer Service Bot", ) # Use the control decorator @@ -35,7 +35,6 @@ async def handle_message(message: str): import agent_control agent_control.init( - agent_name="Customer Service Bot", agent_name="550e8400-e29b-41d4-a716-446655440000", agent_description="Handles customer inquiries and support", agent_version="2.1.0", @@ -54,8 +53,8 @@ One line to set up your agent with full protection: ```python agent_control.init( - agent_name="...", agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="...", ) ``` @@ -103,7 +102,7 @@ Access your agent information: ```python agent = agent_control.current_agent() print(f"Agent: {agent.agent_name}") -print(f"ID: {agent.agent_name}") +print(f"Name: {agent.agent_name}") print(f"Version: {agent.agent_version}") ``` @@ -116,8 +115,8 @@ from agent_control import control, ControlViolationError # Initialize agent_control.init( - agent_name="Customer Support Bot", agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="Customer Support Bot", agent_version="1.0.0" ) @@ -158,11 +157,15 @@ asyncio.run(main()) ```python def init( agent_name: str, - agent_name: str | UUID, agent_description: Optional[str] = None, agent_version: Optional[str] = None, server_url: Optional[str] = None, + api_key: Optional[str] = None, controls_file: Optional[str] = None, + steps: Optional[list[dict]] = None, + conflict_mode: Literal["strict", "overwrite"] = "overwrite", + observability_enabled: Optional[bool] = None, + log_config: Optional[dict[str, Any]] = None, policy_refresh_interval_seconds: int = 60, **kwargs ) -> Agent: @@ -171,12 +174,16 @@ def init( Initialize Agent Control with your agent's information. **Parameters:** -- `agent_name`: Human-readable name -- `agent_name`: UUID string (or UUID instance) +- `agent_name`: Unique identifier for the agent - `agent_description`: Optional description - `agent_version`: Optional version string - `server_url`: Optional server URL (defaults to `AGENT_CONTROL_URL` env var) +- `api_key`: Optional API key (defaults to `AGENT_CONTROL_API_KEY` env var) - `controls_file`: Optional controls file path (auto-discovered if not provided) +- `steps`: Optional step schemas to register with initAgent +- `conflict_mode`: initAgent registration conflict mode (`"strict"` or `"overwrite"`) +- `observability_enabled`: Optional observability toggle +- `log_config`: Optional SDK logging config - `policy_refresh_interval_seconds`: Background cache refresh interval in seconds. Default `60`; set to `0` to disable background refresh. - `**kwargs`: Additional metadata @@ -290,8 +297,8 @@ import agent_control from agent_control import control, ControlViolationError agent_control.init( - agent_name="...", agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_description="...", ) @control() diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index 3facbcb0..fe22c629 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -143,6 +143,10 @@ export const api = { apiClient.POST('/api/v1/policies/{policy_id}/controls/{control_id}', { params: { path: { policy_id: policyId, control_id: controlId } }, }), + removeControl: (policyId: number, controlId: number) => + apiClient.DELETE('/api/v1/policies/{policy_id}/controls/{control_id}', { + params: { path: { policy_id: policyId, control_id: controlId } }, + }), }, observability: { getStats: (params: { diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index 4eb61066..1ff82d88 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -109,7 +109,6 @@ export type ListControlsResponse = // Note: This will be in generated types after running pnpm fetch-api-types export type AgentRef = { agent_name: string; - agent_name: string; }; // Helper type to extract query parameters from operations diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index 2cea8dbe..f30397e5 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -10,12 +10,87 @@ type AddControlToAgentParams = { definition: ControlDefinition; }; +function sanitizePolicyName(agentId: string) { + return `policy-${agentId}` + .toLowerCase() + .replace(/[^a-z0-9_-]/g, '-') + .slice(0, 255); +} + +async function ensureAgentPolicy(agentId: string): Promise { + const { + data: existingPolicy, + error: getPolicyError, + response: getPolicyResponse, + } = await api.agents.getPolicy(agentId); + + if (!getPolicyError && existingPolicy) { + return existingPolicy.policy_id; + } + + if (getPolicyResponse?.status !== 404) { + throw parseApiError( + getPolicyError, + 'Failed to fetch agent policy', + getPolicyResponse?.status + ); + } + + const policyNameBase = sanitizePolicyName(agentId); + const policyNameCandidates = [ + policyNameBase, + `${policyNameBase}-${Date.now()}`, + ]; + + let createdPolicyId: number | null = null; + for (const candidate of policyNameCandidates) { + const { + data: createdPolicy, + error: createPolicyError, + response: createPolicyResponse, + } = await api.policies.create(candidate); + + if (!createPolicyError && createdPolicy) { + createdPolicyId = createdPolicy.policy_id; + break; + } + + if (createPolicyResponse?.status === 409) { + continue; + } + + throw parseApiError( + createPolicyError, + 'Failed to create policy for agent', + createPolicyResponse?.status + ); + } + + if (createdPolicyId === null) { + throw new Error('Unable to create a unique policy for this agent'); + } + + const { error: setPolicyError, response: setPolicyResponse } = + await api.agents.setPolicy(agentId, createdPolicyId); + + if (setPolicyError) { + throw parseApiError( + setPolicyError, + 'Failed to assign policy to agent', + setPolicyResponse?.status + ); + } + + return createdPolicyId; +} + /** * Mutation hook to add a control to an agent * Flow: * 1. Create the control * 2. Set control data (definition) - * 3. Associate the control directly with the agent + * 3. Ensure the agent has a policy + * 4. Add the control to that policy */ export function useAddControlToAgent() { const queryClient = useQueryClient(); @@ -60,14 +135,17 @@ export function useAddControlToAgent() { ); } - // Step 3: Associate control directly with the agent + // Step 3: Ensure the agent has a policy. + const policyId = await ensureAgentPolicy(agentId); + + // Step 4: Add control to policy. const { error: associateError, response: associateResponse } = - await api.agents.addControl(agentId, createdControlId); + await api.policies.addControl(policyId, createdControlId); if (associateError) { throw parseApiError( associateError, - 'Failed to associate control with agent', + 'Failed to add control to agent policy', associateResponse?.status ); } diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-delete-control.ts index 8cfe3c20..d0efba0f 100644 --- a/ui/src/core/hooks/query-hooks/use-delete-control.ts +++ b/ui/src/core/hooks/query-hooks/use-delete-control.ts @@ -10,8 +10,8 @@ type RemoveControlFromAgentParams = { export type RemoveControlFromAgentResult = { success: boolean; - removed_direct_association?: boolean; - control_still_active?: boolean; + removed_from_policy?: boolean; + no_policy_assigned?: boolean; }; /** @@ -25,20 +25,41 @@ export function useRemoveControlFromAgent() { agentId, controlId, }: RemoveControlFromAgentParams) => { - const { data, error, response } = await api.agents.removeControl( - agentId, + const { + data: policyData, + error: policyError, + response: policyResponse, + } = await api.agents.getPolicy(agentId); + + if (policyResponse?.status === 404) { + return { success: true, no_policy_assigned: true }; + } + + if (policyError || !policyData) { + throw parseApiError( + policyError, + 'Failed to fetch agent policy', + policyResponse?.status + ); + } + + const { data, error, response } = await api.policies.removeControl( + policyData.policy_id, controlId ); if (error) { throw parseApiError( error, - 'Failed to remove control from agent', + 'Failed to remove control from agent policy', response?.status ); } - return (data ?? { success: true }) as RemoveControlFromAgentResult; + return { + success: data?.success ?? true, + removed_from_policy: true, + } satisfies RemoveControlFromAgentResult; }, onSuccess: (_data, variables) => { queryClient.invalidateQueries({ diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 2e94ab3e..c4100fe7 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -27,8 +27,8 @@ export function useDeleteControlFlow({ children: ( Remove "{control.name}" from this agent? This only removes - the association for this agent and does not delete the control - globally. + it from the agent's assigned policy and does not delete the + control globally. ), labels: { confirm: 'Remove', cancel: 'Cancel' }, @@ -46,25 +46,18 @@ export function useDeleteControlFlow({ }, { onSuccess: (result: RemoveControlFromAgentResult) => { - const removedDirect = result.removed_direct_association ?? true; - const stillActive = result.control_still_active ?? false; - - if (!removedDirect) { + if (result.no_policy_assigned) { notifications.show({ - title: 'Control is linked indirectly', - message: `"${control.name}" has no direct link on this agent. Remove its inherited link to disable it.`, + title: 'No policy assigned', + message: `"${control.name}" was not removed because this agent has no assigned policy.`, color: 'yellow', }); return; } notifications.show({ - title: stillActive - ? 'Direct association removed' - : 'Control removed', - message: stillActive - ? `"${control.name}" is still active through another inherited link.` - : `"${control.name}" has been removed from this agent.`, + title: 'Control removed', + message: `"${control.name}" has been removed from this agent policy.`, color: 'green', }); if (selectedControl?.id === control.id) { 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 4ad88ba7..1aae74b9 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 @@ -267,8 +267,8 @@ export function ControlStoreModal({ header: 'Used by', size: 150, cell: ({ row }) => { - const count = row.original.used_by_agents_count ?? 0; - if (count === 0) { + const usedByAgent = row.original.used_by_agent; + if (!usedByAgent) { return ( — @@ -277,7 +277,7 @@ export function ControlStoreModal({ } return ( - {count} {count === 1 ? 'agent' : 'agents'} + {usedByAgent.agent_name} ); }, diff --git a/ui/src/core/page-components/home/home.tsx b/ui/src/core/page-components/home/home.tsx index 8b57fd80..ff363cb0 100644 --- a/ui/src/core/page-components/home/home.tsx +++ b/ui/src/core/page-components/home/home.tsx @@ -44,8 +44,8 @@ function EmptyAgentsState() { from agent_control import control, ControlViolationError agent_control.init( - agent_name="Customer Support Agent", agent_name="support-agent-v1", + agent_description="Customer Support Agent", server_url="http://localhost:8000", ) From 1bd3acbc12472d5bc4fae86a6338d20f6dc8a854 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 15:41:02 -0800 Subject: [PATCH 22/32] fix(server): restore agent policy/control association semantics --- models/src/agent_control_models/server.py | 61 +- ...nt_policy_m2m_and_direct_agent_controls.py | 90 +++ .../agent_control_server/endpoints/agents.py | 666 ++++++++++++------ .../endpoints/controls.py | 149 ++-- server/src/agent_control_server/models.py | 34 +- .../agent_control_server/services/controls.py | 33 +- server/tests/test_error_handling.py | 10 + server/tests/test_services_controls.py | 39 +- 8 files changed, 796 insertions(+), 286 deletions(-) create mode 100644 server/alembic/versions/4b8c7d4a1f31_agent_policy_m2m_and_direct_agent_controls.py diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index 693c439a..3ac0467d 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -176,7 +176,7 @@ class InitAgentResponse(BaseModel): ) controls: list[Control] = Field( default_factory=list, - description="Active protection controls for the agent (if policy assigned)", + description="Active protection controls for the agent", ) overwrite_applied: bool = Field( default=False, @@ -201,24 +201,37 @@ class CreatePolicyResponse(BaseModel): policy_id: int = Field(description="Identifier of the created policy") +class GetAgentPoliciesResponse(BaseModel): + policy_ids: list[int] = Field( + default_factory=list, description="IDs of policies associated with the agent" + ) + + class SetPolicyResponse(BaseModel): - success: bool = Field(description="Whether the policy was successfully assigned") + """Compatibility response for singular policy assignment endpoint.""" + + success: bool = Field(description="Whether the request succeeded") old_policy_id: int | None = Field( - default=None, description="Previous policy id if one was replaced" + default=None, + description="Previously associated policy ID, if any", ) class GetPolicyResponse(BaseModel): - policy_id: int = Field(description="Identifier of the policy assigned to the agent") + """Compatibility response for singular policy retrieval endpoint.""" + + policy_id: int = Field(description="Associated policy ID") class DeletePolicyResponse(BaseModel): - success: bool = Field(description="Whether the policy was successfully removed") + """Compatibility response for singular policy deletion endpoint.""" + + success: bool = Field(description="Whether the request succeeded") class AgentControlsResponse(BaseModel): controls: list[Control] = Field( - description="List of controls associated with the agent via its policy" + description="List of active controls associated with the agent" ) @@ -248,6 +261,18 @@ class AssocResponse(BaseModel): success: bool = Field(description="Whether the association change succeeded") +class RemoveAgentControlResponse(BaseModel): + """Response for removing a direct agent-control association.""" + + success: bool = Field(description="Whether the request succeeded") + removed_direct_association: bool = Field( + description="True if a direct agent-control link was removed" + ) + control_still_active: bool = Field( + description="True if the control remains active via policy association(s)" + ) + + class GetControlDataResponse(BaseModel): data: ControlDefinition = Field(description="Control data payload") @@ -310,12 +335,18 @@ class AgentSummary(BaseModel): """Summary of an agent for list responses.""" agent_name: str = Field(..., description="Unique identifier of the agent") - policy_id: int | None = Field(None, description="ID of assigned policy, if any") + policy_id: int | None = Field( + default=None, + description="Deprecated: first associated policy ID, if any", + ) + policy_ids: list[int] = Field( + default_factory=list, description="IDs of policies associated with the agent" + ) 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") evaluator_count: int = Field(0, description="Number of evaluators registered with the agent") active_controls_count: int = Field( - 0, description="Number of active controls from agent's policy" + 0, description="Number of active controls for this agent" ) @@ -345,7 +376,7 @@ class ListAgentsResponse(BaseModel): class AgentRef(BaseModel): """Reference to an agent (for listing which agents use a control).""" - agent_name: str = Field(..., description="Agent identifier") + agent_name: str = Field(..., description="Agent name") class ControlSummary(BaseModel): @@ -360,6 +391,10 @@ class ControlSummary(BaseModel): stages: list[str] | None = Field(None, description="Evaluation stages in scope") tags: list[str] = Field(default_factory=list, description="Control tags") used_by_agent: AgentRef | None = Field(None, description="Agent using this control") + # TODO: Follow-up with full `used_by_agents` list for richer attribution. + used_by_agents_count: int = Field( + 0, description="Number of unique agents using this control" + ) class ListControlsResponse(BaseModel): @@ -374,9 +409,17 @@ class DeleteControlResponse(BaseModel): success: bool = Field(..., description="Whether the control was deleted") dissociated_from: list[int] = Field( + default_factory=list, + description="Deprecated: policy IDs the control was removed from before deletion", + ) + dissociated_from_policies: list[int] = Field( default_factory=list, description="Policy IDs the control was removed from before deletion", ) + dissociated_from_agents: list[str] = Field( + default_factory=list, + description="Agent names the control was removed from before deletion", + ) class PatchControlRequest(BaseModel): diff --git a/server/alembic/versions/4b8c7d4a1f31_agent_policy_m2m_and_direct_agent_controls.py b/server/alembic/versions/4b8c7d4a1f31_agent_policy_m2m_and_direct_agent_controls.py new file mode 100644 index 00000000..2a4962dc --- /dev/null +++ b/server/alembic/versions/4b8c7d4a1f31_agent_policy_m2m_and_direct_agent_controls.py @@ -0,0 +1,90 @@ +"""agent policy m2m and direct agent controls + +Revision ID: 4b8c7d4a1f31 +Revises: 58920e6807fe +Create Date: 2026-03-02 16:15:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "4b8c7d4a1f31" +down_revision = "58920e6807fe" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "agent_controls", + sa.Column("agent_name", sa.String(length=255), nullable=False), + sa.Column("control_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["agent_name"], ["agents.name"]), + sa.ForeignKeyConstraint(["control_id"], ["controls.id"]), + sa.PrimaryKeyConstraint("agent_name", "control_id"), + ) + op.create_index(op.f("ix_agent_controls_agent_name"), "agent_controls", ["agent_name"]) + op.create_index(op.f("ix_agent_controls_control_id"), "agent_controls", ["control_id"]) + + op.create_table( + "agent_policies", + sa.Column("agent_name", sa.String(length=255), nullable=False), + sa.Column("policy_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(["agent_name"], ["agents.name"]), + sa.ForeignKeyConstraint(["policy_id"], ["policies.id"]), + sa.PrimaryKeyConstraint("agent_name", "policy_id"), + ) + op.create_index(op.f("ix_agent_policies_agent_name"), "agent_policies", ["agent_name"]) + op.create_index(op.f("ix_agent_policies_policy_id"), "agent_policies", ["policy_id"]) + + op.execute( + sa.text( + """ + INSERT INTO agent_policies (agent_name, policy_id) + SELECT name, policy_id + FROM agents + WHERE policy_id IS NOT NULL + ON CONFLICT (agent_name, policy_id) DO NOTHING + """ + ) + ) + + op.drop_index(op.f("ix_agents_policy_id"), table_name="agents") + op.drop_constraint(op.f("agents_policy_id_fkey"), "agents", type_="foreignkey") + op.drop_column("agents", "policy_id") + + +def downgrade() -> None: + # NOTE: Downgrade can only restore one policy per agent via agents.policy_id. + # If multiple policies are associated to an agent, all but MIN(policy_id) are lost. + op.add_column("agents", sa.Column("policy_id", sa.Integer(), nullable=True)) + + op.execute( + sa.text( + """ + UPDATE agents AS a + SET policy_id = ap.policy_id + FROM ( + SELECT agent_name, MIN(policy_id) AS policy_id + FROM agent_policies + GROUP BY agent_name + ) AS ap + WHERE a.name = ap.agent_name + """ + ) + ) + + op.create_foreign_key(op.f("agents_policy_id_fkey"), "agents", "policies", ["policy_id"], ["id"]) + op.create_index(op.f("ix_agents_policy_id"), "agents", ["policy_id"]) + + op.drop_index(op.f("ix_agent_policies_policy_id"), table_name="agent_policies") + op.drop_index(op.f("ix_agent_policies_agent_name"), table_name="agent_policies") + op.drop_table("agent_policies") + + # Direct agent-control links have no representation in the downgraded schema. + op.drop_index(op.f("ix_agent_controls_control_id"), table_name="agent_controls") + op.drop_index(op.f("ix_agent_controls_agent_name"), table_name="agent_controls") + op.drop_table("agent_controls") diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 82cecfc9..390a9967 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -7,9 +7,11 @@ from agent_control_models.server import ( AgentControlsResponse, AgentSummary, + AssocResponse, ConflictMode, DeletePolicyResponse, EvaluatorSchema, + GetAgentPoliciesResponse, GetAgentResponse, GetPolicyResponse, InitAgentEvaluatorRemoval, @@ -20,13 +22,15 @@ PaginationInfo, PatchAgentRequest, PatchAgentResponse, + RemoveAgentControlResponse, SetPolicyResponse, StepKey, ) from fastapi import APIRouter, Depends from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import BaseModel, ValidationError -from sqlalchemy import func, or_, select +from sqlalchemy import delete, func, or_, select, union_all +from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -43,6 +47,8 @@ AgentData, Control, Policy, + agent_controls, + agent_policies, policy_controls, ) from ..services.agent_names import normalize_agent_name_or_422 @@ -51,6 +57,7 @@ parse_evaluator_ref_full, validate_config_against_schema, ) +from ..services.query_utils import escape_like_pattern from ..services.schema_compat import ( check_schema_compatibility, format_compatibility_error, @@ -85,18 +92,8 @@ def _get_builtin_evaluator_names() -> set[str]: return _BUILTIN_EVALUATOR_NAMES -async def _validate_policy_controls_for_agent( - agent: Agent, policy_id: int, db: AsyncSession -) -> list[str]: - """Validate all controls in a policy can run on this agent. - - Checks that agent-scoped evaluators referenced by controls: - 1. Exist on the agent (registered via initAgent) - 2. Have config that validates against the evaluator's schema - - Returns: - List of error messages (empty if all valid) - """ +def _validate_controls_for_agent(agent: Agent, controls: list[Control]) -> list[str]: + """Validate controls can run on this agent.""" errors: list[str] = [] # Parse agent's registered evaluators @@ -107,9 +104,6 @@ async def _validate_policy_controls_for_agent( agent_evaluators = {e.name: e for e in (agent_data.evaluators or [])} - # Get all controls for this policy - controls = await list_controls_for_policy(policy_id, db) - for control in controls: if not control.data: continue @@ -155,6 +149,14 @@ async def _validate_policy_controls_for_agent( return errors +async def _validate_policy_controls_for_agent( + agent: Agent, policy_id: int, db: AsyncSession +) -> list[str]: + """Validate all controls in a policy can run on this agent.""" + controls = await list_controls_for_policy(policy_id, db) + return _validate_controls_for_agent(agent, controls) + + def _step_registration_changed(existing_step: StepSchema, incoming_step: StepSchema) -> bool: """Return True when non-key step registration fields differ.""" return ( @@ -177,7 +179,7 @@ async def _build_overwrite_evaluator_removals( db: AsyncSession, ) -> list[InitAgentEvaluatorRemoval]: """Build evaluator removal details, including active-control references.""" - if not removed_evaluators or agent.policy_id is None: + if not removed_evaluators: return [InitAgentEvaluatorRemoval(name=name) for name in sorted(removed_evaluators)] try: @@ -239,7 +241,7 @@ async def list_agents( """ List all registered agents with cursor-based pagination. - Returns a summary of each agent including identifier, policy assignment, + Returns a summary of each agent including identifier, policy associations, and counts of registered steps and evaluators. Args: @@ -255,7 +257,9 @@ async def list_agents( limit = min(max(1, limit), _MAX_PAGINATION_LIMIT) # Build base filter for name search - name_filter = Agent.name.ilike(f"%{name}%") if name else None + name_filter = ( + Agent.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") if name else None + ) # Get total count (with name filter if provided) count_query = select(func.count()).select_from(Agent) @@ -303,32 +307,54 @@ async def list_agents( if has_more and agents: 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[str, int] = {} + policy_ids_map: dict[str, list[int]] = {} if agents: + agent_names = [agent.name for agent in agents] + + policy_ids_query = ( + select( + agent_policies.c.agent_name, + agent_policies.c.policy_id, + ) + .where(agent_policies.c.agent_name.in_(agent_names)) + .order_by(agent_policies.c.agent_name, agent_policies.c.policy_id) + ) + policy_ids_result = await db.execute(policy_ids_query) + for assoc_agent_name, policy_id in policy_ids_result.all(): + policy_ids_map.setdefault(assoc_agent_name, []).append(policy_id) + + policy_associations = ( + select( + agent_policies.c.agent_name.label("agent_name"), + policy_controls.c.control_id.label("control_id"), + ) + .select_from( + agent_policies.join( + policy_controls, agent_policies.c.policy_id == policy_controls.c.policy_id + ) + ) + .where(agent_policies.c.agent_name.in_(agent_names)) + ) + direct_associations = select( + agent_controls.c.agent_name.label("agent_name"), + agent_controls.c.control_id.label("control_id"), + ).where(agent_controls.c.agent_name.in_(agent_names)) + all_associations = union_all(policy_associations, direct_associations).subquery() + control_counts_query = ( select( - Agent.name, - func.count(func.distinct(policy_controls.c.control_id)).label("count"), + all_associations.c.agent_name, + func.count(func.distinct(all_associations.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) + .join(Control, all_associations.c.control_id == Control.id) .where( - 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) or_( Control.data["enabled"].astext == "true", ~Control.data.has_key("enabled"), ), ) - .group_by(Agent.name) + .group_by(all_associations.c.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()} @@ -351,10 +377,14 @@ async def list_agents( # Get active controls count from batched query result active_controls = control_counts_map.get(agent.name, 0) + policy_ids = policy_ids_map.get(agent.name, []) + primary_policy_id = policy_ids[0] if policy_ids else None + summaries.append( AgentSummary( agent_name=agent.name, - policy_id=agent.policy_id, + policy_id=primary_policy_id, + policy_ids=policy_ids, created_at=agent.created_at.isoformat() if agent.created_at else None, step_count=step_count, evaluator_count=evaluator_count, @@ -729,10 +759,7 @@ async def init_agent( operation="update", ) - # 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.name, db) + controls = await list_controls_for_agent(existing.name, db) return InitAgentResponse( created=created, @@ -818,46 +845,99 @@ async def get_agent(agent_name: str, db: AsyncSession = Depends(get_async_db)) - ) +async def _get_agent_or_404(agent_name: str, db: AsyncSession) -> Agent: + """Get an agent or raise AGENT_NOT_FOUND.""" + normalized_agent_name = normalize_agent_name_or_422(agent_name) + result = await db.execute(select(Agent).where(Agent.name == normalized_agent_name)) + agent: Agent | None = result.scalars().first() + if agent is None: + raise NotFoundError( + error_code=ErrorCode.AGENT_NOT_FOUND, + detail=f"Agent with name '{normalized_agent_name}' not found", + resource="Agent", + resource_id=normalized_agent_name, + hint="Verify the agent name is correct and the agent has been registered.", + ) + return agent + + @router.post( - "/{agent_name}/policy/{policy_id}", - response_model=SetPolicyResponse, - summary="Assign policy to agent", - response_description="Success status with previous policy ID", + "/{agent_name}/policies/{policy_id}", + response_model=AssocResponse, + summary="Associate policy with agent", + response_description="Success confirmation", ) -async def set_agent_policy( +async def add_agent_policy( agent_name: str, policy_id: int, db: AsyncSession = Depends(get_async_db) -) -> SetPolicyResponse: - """ - Assign a policy to an agent, replacing any existing policy assignment. - - The agent will immediately inherit all controls from the assigned policy. +) -> AssocResponse: + """Associate a policy with an agent (idempotent).""" + agent = await _get_agent_or_404(agent_name, db) - Args: - agent_name: Agent identifier - policy_id: ID of the policy to assign - db: Database session (injected) + policy_result = await db.execute(select(Policy).where(Policy.id == policy_id)) + policy: Policy | None = policy_result.scalars().first() + if policy is None: + raise NotFoundError( + error_code=ErrorCode.POLICY_NOT_FOUND, + detail=f"Policy with ID '{policy_id}' not found", + resource="Policy", + resource_id=str(policy_id), + hint="Verify the policy ID is correct and the policy has been created.", + ) - Returns: - SetPolicyResponse with success flag and previous policy ID (if any) + validation_errors = await _validate_policy_controls_for_agent(agent, policy_id, db) + if validation_errors: + raise BadRequestError( + error_code=ErrorCode.POLICY_CONTROL_INCOMPATIBLE, + detail="Policy contains controls incompatible with this agent", + hint="Ensure all controls in the policy are compatible with this agent's evaluators.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator", + code="incompatible", + message=err, + ) + for err in validation_errors + ], + ) - Raises: - 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.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 name '{agent_name}' not found", + try: + stmt = ( + pg_insert(agent_policies) + .values(agent_name=agent.name, policy_id=policy_id) + .on_conflict_do_nothing() + ) + await db.execute(stmt) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to associate policy '%s' with agent '%s'", + policy_id, + agent.name, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to associate policy with agent '{agent.name}': database error", resource="Agent", - resource_id=str(agent_name), - hint="Verify the agent name is correct and the agent has been registered.", + operation="add policy association", ) - # Find policy by id + return AssocResponse(success=True) + + +@router.post( + "/{agent_name}/policy/{policy_id}", + response_model=SetPolicyResponse, + summary="Assign policy to agent (compatibility)", + response_description="Success status with previous policy ID", +) +async def set_agent_policy( + agent_name: str, policy_id: int, db: AsyncSession = Depends(get_async_db) +) -> SetPolicyResponse: + """Compatibility endpoint that replaces all policy associations with one policy.""" + agent = await _get_agent_or_404(agent_name, db) + policy_result = await db.execute(select(Policy).where(Policy.id == policy_id)) policy: Policy | None = policy_result.scalars().first() if policy is None: @@ -869,7 +949,6 @@ async def set_agent_policy( hint="Verify the policy ID is correct and the policy has been created.", ) - # Validate controls can run on this agent validation_errors = await _validate_policy_controls_for_agent(agent, policy_id, db) if validation_errors: raise BadRequestError( @@ -887,19 +966,28 @@ async def set_agent_policy( ], ) - # Store old policy ID if exists - old_policy_id: int | None = None - if agent.policy_id is not None: - old_policy_id = agent.policy_id + existing_policies_result = await db.execute( + select(agent_policies.c.policy_id) + .where(agent_policies.c.agent_name == agent.name) + .order_by(agent_policies.c.policy_id) + ) + existing_policy_ids = [row[0] for row in existing_policies_result.all()] + old_policy_id = existing_policy_ids[0] if existing_policy_ids else None - # Assign new policy - agent.policy_id = policy.id try: + await db.execute(delete(agent_policies).where(agent_policies.c.agent_name == agent.name)) + await db.execute( + pg_insert(agent_policies) + .values(agent_name=agent.name, policy_id=policy_id) + .on_conflict_do_nothing() + ) await db.commit() except Exception: await db.rollback() _logger.error( - f"Failed to assign policy '{policy_id}' to agent '{agent.name}' ({agent_name})", + "Failed to assign policy '%s' to agent '%s'", + policy_id, + agent.name, exc_info=True, ) raise DatabaseError( @@ -911,139 +999,332 @@ async def set_agent_policy( return SetPolicyResponse(success=True, old_policy_id=old_policy_id) +@router.get( + "/{agent_name}/policies", + response_model=GetAgentPoliciesResponse, + summary="List policies associated with agent", + response_description="List of policy IDs", +) +async def get_agent_policies( + agent_name: str, db: AsyncSession = Depends(get_async_db) +) -> GetAgentPoliciesResponse: + """List policy IDs associated with an agent.""" + agent = await _get_agent_or_404(agent_name, db) + result = await db.execute( + select(agent_policies.c.policy_id) + .where(agent_policies.c.agent_name == agent.name) + .order_by(agent_policies.c.policy_id) + ) + return GetAgentPoliciesResponse(policy_ids=[row[0] for row in result.all()]) + + @router.get( "/{agent_name}/policy", response_model=GetPolicyResponse, - summary="Get agent's assigned policy", + summary="Get agent's assigned policy (compatibility)", response_description="Policy ID", ) async def get_agent_policy( agent_name: str, db: AsyncSession = Depends(get_async_db) ) -> GetPolicyResponse: - """ - Retrieve the policy currently assigned to an agent. + """Compatibility endpoint that returns the first associated policy.""" + agent = await _get_agent_or_404(agent_name, db) + policy_result = await db.execute( + select(Policy.id) + .join(agent_policies, agent_policies.c.policy_id == Policy.id) + .where(agent_policies.c.agent_name == agent.name) + .order_by(Policy.id) + .limit(1) + ) + policy_id = policy_result.scalars().first() + if policy_id is None: + raise NotFoundError( + 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_name}/policies/{policy_id}.", + ) + return GetPolicyResponse(policy_id=policy_id) - Args: - agent_name: Agent identifier - db: Database session (injected) - Returns: - GetPolicyResponse with policy ID +@router.delete( + "/{agent_name}/policies/{policy_id}", + response_model=AssocResponse, + summary="Remove policy association from agent", + response_description="Success confirmation", +) +async def remove_agent_policy( + agent_name: str, policy_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Remove a policy association from an agent. - Raises: - HTTPException 404: Agent not found or agent has no policy assigned + Idempotent for existing resources: removing a non-associated link is a no-op. + Missing agent/policy resources still return 404. """ - agent_name = normalize_agent_name_or_422(agent_name) - # Find agent - 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 name '{agent_name}' not found", - resource="Agent", - resource_id=str(agent_name), - hint="Verify the agent name is correct and the agent has been registered.", - ) + agent = await _get_agent_or_404(agent_name, db) - # Check if agent has a policy - if agent.policy_id is None: + policy_result = await db.execute(select(Policy.id).where(Policy.id == policy_id)) + if policy_result.first() is None: raise NotFoundError( error_code=ErrorCode.POLICY_NOT_FOUND, - detail=f"Agent '{agent.name}' has no policy assigned", + detail=f"Policy with ID '{policy_id}' not found", resource="Policy", - hint="Assign a policy to the agent using POST /{agent_name}/policy/{policy_id}.", + resource_id=str(policy_id), + hint="Verify the policy ID is correct and the policy has been created.", ) - # Find policy - policy_result = await db.execute(select(Policy).where(Policy.id == agent.policy_id)) - policy: Policy | None = policy_result.scalars().first() - if policy is None: - raise NotFoundError( - error_code=ErrorCode.POLICY_NOT_FOUND, + try: + await db.execute( + delete(agent_policies).where( + (agent_policies.c.agent_name == agent.name) + & (agent_policies.c.policy_id == policy_id) + ) + ) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to remove policy '%s' from agent '%s'", + policy_id, + agent.name, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to remove policy association from agent '{agent.name}': database error", + resource="Agent", + operation="remove policy association", + ) + + return AssocResponse(success=True) + + +@router.delete( + "/{agent_name}/policies", + response_model=AssocResponse, + summary="Remove all policy associations from agent", + response_description="Success confirmation", +) +async def remove_all_agent_policies( + agent_name: str, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Remove all policy associations from an agent.""" + agent = await _get_agent_or_404(agent_name, db) + + try: + await db.execute(delete(agent_policies).where(agent_policies.c.agent_name == agent.name)) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to remove all policies from agent '%s'", + agent.name, + exc_info=True, + ) + raise DatabaseError( detail=( - f"Policy with ID '{agent.policy_id}' not found " - f"(referenced by agent '{agent.name}')" + f"Failed to remove policy associations from agent '{agent.name}': " + "database error" ), - resource="Policy", - resource_id=str(agent.policy_id), - hint="The referenced policy may have been deleted. Assign a new policy to the agent.", + resource="Agent", + operation="remove all policy associations", ) - return GetPolicyResponse(policy_id=policy.id) + return AssocResponse(success=True) @router.delete( "/{agent_name}/policy", response_model=DeletePolicyResponse, - summary="Remove agent's policy assignment", + summary="Remove agent's policy assignment (compatibility)", response_description="Success confirmation", ) async def delete_agent_policy( agent_name: str, db: AsyncSession = Depends(get_async_db) ) -> DeletePolicyResponse: - """ - Remove the policy assignment from an agent. + """Compatibility endpoint that removes all policy associations.""" + agent = await _get_agent_or_404(agent_name, db) - The agent will no longer have any protection controls active. + existing_policy_result = await db.execute( + select(agent_policies.c.policy_id) + .where(agent_policies.c.agent_name == agent.name) + .limit(1) + ) + if existing_policy_result.first() is None: + raise NotFoundError( + error_code=ErrorCode.POLICY_NOT_FOUND, + detail=f"Agent '{agent.name}' has no policy assigned", + resource="Policy", + hint="The agent does not have a policy to remove.", + ) - Args: - agent_name: Agent identifier - db: Database session (injected) + try: + await db.execute(delete(agent_policies).where(agent_policies.c.agent_name == agent.name)) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to remove policy associations from agent '%s'", + agent.name, + exc_info=True, + ) + raise DatabaseError( + detail=( + f"Failed to remove policy associations from agent '{agent.name}': " + "database error" + ), + resource="Agent", + operation="remove policy", + ) - Returns: - DeletePolicyResponse with success flag + return DeletePolicyResponse(success=True) - Raises: - 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.name == agent_name)) - agent: Agent | None = result.scalars().first() - if agent is None: + +@router.post( + "/{agent_name}/controls/{control_id}", + response_model=AssocResponse, + summary="Associate control directly with agent", + response_description="Success confirmation", +) +async def add_agent_control( + agent_name: str, control_id: int, db: AsyncSession = Depends(get_async_db) +) -> AssocResponse: + """Associate a control directly with an agent (idempotent).""" + agent = await _get_agent_or_404(agent_name, db) + + control_result = await db.execute(select(Control).where(Control.id == control_id)) + control: Control | None = control_result.scalars().first() + if control is None: raise NotFoundError( - error_code=ErrorCode.AGENT_NOT_FOUND, - detail=f"Agent with name '{agent_name}' not found", + error_code=ErrorCode.CONTROL_NOT_FOUND, + detail=f"Control with ID '{control_id}' not found", + resource="Control", + resource_id=str(control_id), + hint="Verify the control ID is correct and the control has been created.", + ) + + validation_errors = _validate_controls_for_agent(agent, [control]) + if validation_errors: + raise BadRequestError( + error_code=ErrorCode.POLICY_CONTROL_INCOMPATIBLE, + detail="Control is incompatible with this agent", + hint="Ensure the control is compatible with this agent's evaluators.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator", + code="incompatible", + message=err, + ) + for err in validation_errors + ], + ) + + try: + stmt = ( + pg_insert(agent_controls) + .values(agent_name=agent.name, control_id=control_id) + .on_conflict_do_nothing() + ) + await db.execute(stmt) + await db.commit() + except Exception: + await db.rollback() + _logger.error( + "Failed to associate control '%s' with agent '%s'", + control_id, + agent.name, + exc_info=True, + ) + raise DatabaseError( + detail=f"Failed to associate control with agent '{agent.name}': database error", resource="Agent", - resource_id=str(agent_name), - hint="Verify the agent name is correct and the agent has been registered.", + operation="add control association", ) - # Check if agent has a policy - if agent.policy_id is None: + return AssocResponse(success=True) + + +@router.delete( + "/{agent_name}/controls/{control_id}", + response_model=RemoveAgentControlResponse, + summary="Remove direct control association from agent", + response_description="Success confirmation", +) +async def remove_agent_control( + agent_name: str, control_id: int, db: AsyncSession = Depends(get_async_db) +) -> RemoveAgentControlResponse: + """Remove a direct control association from an agent (idempotent).""" + agent = await _get_agent_or_404(agent_name, db) + + control_result = await db.execute(select(Control.id).where(Control.id == control_id)) + if control_result.first() is None: raise NotFoundError( - error_code=ErrorCode.POLICY_NOT_FOUND, - detail=f"Agent '{agent.name}' has no policy assigned", - resource="Policy", - hint="The agent does not have a policy to remove.", + error_code=ErrorCode.CONTROL_NOT_FOUND, + detail=f"Control with ID '{control_id}' not found", + resource="Control", + resource_id=str(control_id), + hint="Verify the control ID is correct and the control has been created.", ) - # Remove policy assignment - agent.policy_id = None try: + remove_direct_stmt = ( + delete(agent_controls) + .where( + (agent_controls.c.agent_name == agent.name) + & (agent_controls.c.control_id == control_id) + ) + .returning(agent_controls.c.control_id) + ) + remove_direct_result = await db.execute(remove_direct_stmt) + removed_direct_association = remove_direct_result.first() is not None + + # The control may still be active for this agent if inherited from policy association(s). + policy_inheritance_result = await db.execute( + select(policy_controls.c.control_id) + .select_from( + agent_policies.join( + policy_controls, + agent_policies.c.policy_id == policy_controls.c.policy_id, + ) + ) + .where( + (agent_policies.c.agent_name == agent.name) + & (policy_controls.c.control_id == control_id) + ) + .limit(1) + ) + control_still_active = policy_inheritance_result.first() is not None + await db.commit() except Exception: await db.rollback() _logger.error( - f"Failed to remove policy from agent '{agent.name}' ({agent_name})", + "Failed to remove control '%s' from agent '%s'", + control_id, + agent.name, exc_info=True, ) raise DatabaseError( - detail=f"Failed to remove policy from agent '{agent.name}': database error", + detail=( + f"Failed to remove control association from agent '{agent.name}': " + "database error" + ), resource="Agent", - operation="remove policy", + operation="remove control association", ) - return DeletePolicyResponse(success=True) + return RemoveAgentControlResponse( + success=True, + removed_direct_association=removed_direct_association, + control_still_active=control_still_active, + ) @router.get( "/{agent_name}/controls", response_model=AgentControlsResponse, summary="List agent's active controls", - response_description="List of controls from agent's policy", + response_description="List of controls from agent policy and direct associations", ) async def list_agent_controls( agent_name: str, db: AsyncSession = Depends(get_async_db) @@ -1051,35 +1332,20 @@ async def list_agent_controls( """ List all protection controls active for an agent. - Controls are inherited from the agent's assigned policy. - Returns an empty list if the agent has no policy. + Controls include the union of policy-derived and directly associated controls. Args: agent_name: Agent identifier db: Database session (injected) Returns: - AgentControlsResponse with list of controls (empty if no policy) + AgentControlsResponse with list of active controls Raises: HTTPException 404: Agent not found """ - 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 name '{agent_name}' not found", - resource="Agent", - 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_name, db) + agent = await _get_agent_or_404(agent_name, db) + controls = await list_controls_for_agent(agent.name, db) return AgentControlsResponse(controls=controls) @@ -1338,37 +1604,35 @@ async def patch_agent( if request.remove_evaluators: remove_evaluator_set = set(request.remove_evaluators) - # 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.name, db) - referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) - - for ctrl in controls: - evaluator_ref = ctrl.control.evaluator.name - if ":" in evaluator_ref: - ref_agent, ref_eval = evaluator_ref.split(":", 1) - # Check if this control references an evaluator we're removing - # AND it's scoped to this agent (by name match) - if ref_agent == agent.name and ref_eval in remove_evaluator_set: - referencing_controls.append((ctrl.name, ref_eval)) - - if referencing_controls: - raise ConflictError( - error_code=ErrorCode.EVALUATOR_IN_USE, - detail="Cannot remove evaluators: active controls reference them", - resource="Evaluator", - hint="Remove or update the controls that reference these evaluators first.", - errors=[ - ValidationErrorItem( - resource="Control", - field="evaluator.name", - code="in_use", - message=f"Control '{ctrl}' uses evaluator '{ev}'", - ) - for ctrl, ev in referencing_controls - ], - ) + # Check if any active controls reference evaluators being removed. + controls = await list_controls_for_agent(agent.name, db) + referencing_controls: list[tuple[str, str]] = [] # (control_name, evaluator) + + for ctrl in controls: + evaluator_ref = ctrl.control.evaluator.name + if ":" in evaluator_ref: + ref_agent, ref_eval = evaluator_ref.split(":", 1) + # Check if this control references an evaluator we're removing + # AND it's scoped to this agent (by name match) + if ref_agent == agent.name and ref_eval in remove_evaluator_set: + referencing_controls.append((ctrl.name, ref_eval)) + + if referencing_controls: + raise ConflictError( + error_code=ErrorCode.EVALUATOR_IN_USE, + detail="Cannot remove evaluators: active controls reference them", + resource="Evaluator", + hint="Remove or update the controls that reference these evaluators first.", + errors=[ + ValidationErrorItem( + resource="Control", + field="evaluator.name", + code="in_use", + message=f"Control '{ctrl}' uses evaluator '{ev}'", + ) + for ctrl, ev in referencing_controls + ], + ) new_evaluators = [] for ev in data_model.evaluators or []: diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index 97a85f17..12069c7e 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -21,7 +21,7 @@ from fastapi import APIRouter, Depends, Query from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import ValidationError -from sqlalchemy import delete, func, or_, select +from sqlalchemy import Integer, String, delete, func, literal, or_, select, union_all from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -32,11 +32,12 @@ NotFoundError, ) from ..logging_utils import get_logger -from ..models import Agent, AgentData, Control, Policy, policy_controls +from ..models import Agent, AgentData, Control, agent_controls, agent_policies, policy_controls from ..services.evaluator_utils import ( parse_evaluator_ref_full, validate_config_against_schema, ) +from ..services.query_utils import escape_like_pattern # Pagination constants _DEFAULT_PAGINATION_LIMIT = 20 @@ -507,7 +508,7 @@ async def list_controls( # Apply name filter (case-insensitive partial match) if name is not None: - query = query.where(Control.name.ilike(f"%{name}%")) + query = query.where(Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\")) # Don't apply to count_query - total should be pre-filter # Apply JSONB filters at database level @@ -554,7 +555,9 @@ async def list_controls( # Get total count (with same filters, but without cursor/limit) total_query = select(func.count()).select_from(Control) if name is not None: - total_query = total_query.where(Control.name.ilike(f"%{name}%")) + total_query = total_query.where( + Control.name.ilike(f"%{escape_like_pattern(name)}%", escape="\\") + ) if enabled is not None: if enabled: total_query = total_query.where( @@ -593,27 +596,45 @@ async def list_controls( if has_more: controls = controls[:-1] - # Build mapping of control_id -> agent that uses it - # Traversal: Control -> policy_controls -> Policy -> Agent + # Build mapping of control_id -> usage attribution + # Traversal includes both: + # - Control -> policy_controls -> agent_policies -> Agent + # - Control -> agent_controls -> Agent control_agent_map: dict[int, AgentRef | None] = {ctrl.id: None for ctrl in controls} + control_agent_names_map: dict[int, set[str]] = {ctrl.id: set() for ctrl in controls} + control_agent_repr_map: dict[int, str | None] = {ctrl.id: None for ctrl in controls} if controls: control_ids = [ctrl.id for ctrl in controls] - agents_query = ( + policy_agents_query = ( select( policy_controls.c.control_id, - Agent.name, + agent_policies.c.agent_name, ) .select_from(policy_controls) - .join(Policy, policy_controls.c.policy_id == Policy.id) - .join(Agent, Agent.policy_id == Policy.id) + .join(agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id) .where(policy_controls.c.control_id.in_(control_ids)) ) + direct_agents_query = ( + select( + agent_controls.c.control_id, + agent_controls.c.agent_name, + ) + .select_from(agent_controls) + .where(agent_controls.c.control_id.in_(control_ids)) + ) + agents_query = union_all(policy_agents_query, direct_agents_query) agents_result = await db.execute(agents_query) for row in agents_result.all(): 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_name=agent_name) + control_agent_names_map[control_id].add(agent_name) + + # Keep a deterministic representative agent for backwards compatibility. + current_repr = control_agent_repr_map[control_id] + if current_repr is None or agent_name < current_repr: + control_agent_repr_map[control_id] = agent_name + control_agent_map[control_id] = AgentRef( + agent_name=agent_name + ) # Build summaries (filtering already done at DB level) summaries: list[ControlSummary] = [] @@ -632,6 +653,7 @@ async def list_controls( stages=scope.get("stages"), tags=data.get("tags", []), used_by_agent=control_agent_map.get(ctrl.id), + used_by_agents_count=len(control_agent_names_map.get(ctrl.id, set())), ) ) @@ -661,15 +683,15 @@ async def delete_control( control_id: int, force: bool = Query( False, - description="If true, dissociate from all policies before deleting. " - "If false, fail if control is associated with any policy.", + description="If true, dissociate from all policy/agent links before deleting. " + "If false, fail if control is associated with any policy or agent.", ), db: AsyncSession = Depends(get_async_db), ) -> DeleteControlResponse: """ Delete a control by ID. - By default, deletion fails if the control is associated with any policy. + By default, deletion fails if the control is associated with any policy or agent. Use force=true to automatically dissociate and delete. Args: @@ -678,7 +700,7 @@ async def delete_control( db: Database session (injected) Returns: - DeleteControlResponse with success flag and list of dissociated policies + DeleteControlResponse with success flag and dissociation details Raises: HTTPException 404: Control not found @@ -697,48 +719,74 @@ async def delete_control( hint="Verify the control ID is correct and the control has been created.", ) - # Check for associations with policies - assoc_result = await db.execute( - select(policy_controls.c.policy_id).where( - policy_controls.c.control_id == control_id - ) - ) - associated_policy_ids = [row[0] for row in assoc_result.all()] - - if associated_policy_ids and not force: + # Check for associations with policies and direct agent links. + policy_assoc_query = select( + policy_controls.c.policy_id.label("policy_id"), + literal(None, type_=String).label("agent_name"), + ).where(policy_controls.c.control_id == control_id) + agent_assoc_query = select( + literal(None, type_=Integer).label("policy_id"), + agent_controls.c.agent_name.label("agent_name"), + ).where(agent_controls.c.control_id == control_id) + assoc_result = await db.execute(union_all(policy_assoc_query, agent_assoc_query)) + + associated_policy_ids: list[int] = [] + associated_agent_names: list[str] = [] + for policy_id, agent_name in assoc_result.all(): + if policy_id is not None: + associated_policy_ids.append(policy_id) + if agent_name is not None: + associated_agent_names.append(agent_name) + + if (associated_policy_ids or associated_agent_names) and not force: + errors = [ + ValidationErrorItem( + resource="Policy", + field="controls", + code="control_in_use", + message=f"Control is associated with policy ID {pid}", + value=pid, + ) + for pid in associated_policy_ids + ] + [ + ValidationErrorItem( + resource="Agent", + field="controls", + code="control_in_use", + message=f"Control is directly associated with agent '{agent_name}'", + value=agent_name, + ) + for agent_name in associated_agent_names + ] raise ConflictError( error_code=ErrorCode.CONTROL_IN_USE, detail=( f"Control '{control.name}' is associated with " - f"{len(associated_policy_ids)} policy/policies" + f"{len(associated_policy_ids)} policy/policies and " + f"{len(associated_agent_names)} agent(s)" ), resource="Control", resource_id=control.name, hint="Use force=true to dissociate and delete, or remove associations manually first.", - errors=[ - ValidationErrorItem( - resource="Policy", - field="controls", - code="control_in_use", - message=f"Control is associated with policy ID {pid}", - value=pid, - ) - for pid in associated_policy_ids - ], + errors=errors, ) - # Remove associations if force=true - dissociated_from: list[int] = [] + # Remove associations if force=true. + dissociated_from_policies: list[int] = [] + dissociated_from_agents: list[str] = [] if associated_policy_ids: - await db.execute( - delete(policy_controls).where( - policy_controls.c.control_id == control_id - ) - ) - dissociated_from = associated_policy_ids + await db.execute(delete(policy_controls).where(policy_controls.c.control_id == control_id)) + dissociated_from_policies = associated_policy_ids + if associated_agent_names: + await db.execute(delete(agent_controls).where(agent_controls.c.control_id == control_id)) + dissociated_from_agents = associated_agent_names + if dissociated_from_policies or dissociated_from_agents: _logger.info( - f"Dissociated control '{control.name}' ({control_id}) " - f"from {len(dissociated_from)} policy/policies" + "Dissociated control '%s' (%s) from %s policy/policies and %s agent(s)", + control.name, + control_id, + len(dissociated_from_policies), + len(dissociated_from_agents), ) # Delete the control @@ -758,7 +806,12 @@ async def delete_control( operation="delete", ) - return DeleteControlResponse(success=True, dissociated_from=dissociated_from) + return DeleteControlResponse( + success=True, + dissociated_from=dissociated_from_policies, + dissociated_from_policies=dissociated_from_policies, + dissociated_from_agents=dissociated_from_agents, + ) @router.patch( diff --git a/server/src/agent_control_server/models.py b/server/src/agent_control_server/models.py index 383c7681..1b148d1b 100644 --- a/server/src/agent_control_server/models.py +++ b/server/src/agent_control_server/models.py @@ -1,5 +1,5 @@ import datetime as dt -from typing import Any, Optional +from typing import Any from agent_control_models.agent import StepSchema, normalize_agent_name from agent_control_models.base import BaseModel @@ -38,13 +38,31 @@ class AgentData(BaseModel): Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), ) +# Association table for Agent <> Policy many-to-many relationship +agent_policies: Table = Table( + "agent_policies", + Base.metadata, + Column("agent_name", ForeignKey("agents.name"), primary_key=True, index=True), + Column("policy_id", ForeignKey("policies.id"), primary_key=True, index=True), +) + +# Association table for Agent <> Control many-to-many direct relationship +agent_controls: Table = Table( + "agent_controls", + Base.metadata, + Column("agent_name", ForeignKey("agents.name"), primary_key=True, index=True), + Column("control_id", ForeignKey("controls.id"), primary_key=True, index=True), +) + class Policy(Base): __tablename__ = "policies" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) - agents: Mapped[list["Agent"]] = relationship("Agent", back_populates="policy") + agents: Mapped[list["Agent"]] = relationship( + "Agent", secondary=lambda: agent_policies, back_populates="policies" + ) # Many-to-many: Policy <> Control (direct relationship, no ControlSet layer) controls: Mapped[list["Control"]] = relationship( "Control", secondary=lambda: policy_controls, back_populates="policies" @@ -64,6 +82,10 @@ class Control(Base): policies: Mapped[list["Policy"]] = relationship( "Policy", secondary=lambda: policy_controls, back_populates="controls" ) + # Many-to-many backref: Control <> Agent (direct relationship) + agents: Mapped[list["Agent"]] = relationship( + "Agent", secondary=lambda: agent_controls, back_populates="controls" + ) class EvaluatorConfigDB(Base): @@ -97,10 +119,12 @@ class Agent(Base): data: Mapped[dict[str, Any]] = mapped_column( JSONB, server_default=text("'{}'::jsonb"), nullable=False ) - policy_id: Mapped[int | None] = mapped_column( - ForeignKey("policies.id"), nullable=True, index=True + policies: Mapped[list["Policy"]] = relationship( + "Policy", secondary=lambda: agent_policies, back_populates="agents" + ) + controls: Mapped[list["Control"]] = relationship( + "Control", secondary=lambda: agent_controls, back_populates="agents" ) - policy: Mapped[Optional["Policy"]] = relationship("Policy", back_populates="agents") created_at: Mapped[dt.datetime] = mapped_column( DateTime(), server_default=text("CURRENT_TIMESTAMP"), nullable=False, index=True ) diff --git a/server/src/agent_control_server/services/controls.py b/server/src/agent_control_server/services/controls.py index 46f9987d..15a05e9b 100644 --- a/server/src/agent_control_server/services/controls.py +++ b/server/src/agent_control_server/services/controls.py @@ -7,11 +7,11 @@ from agent_control_models.errors import ErrorCode, ValidationErrorItem from agent_control_models.policy import Control as APIControl from pydantic import ValidationError -from sqlalchemy import select +from sqlalchemy import select, union from sqlalchemy.ext.asyncio import AsyncSession from ..errors import APIValidationError -from ..models import Agent, Control, Policy, policy_controls +from ..models import Control, agent_controls, agent_policies, policy_controls _logger = logging.getLogger(__name__) @@ -33,19 +33,30 @@ async def list_controls_for_agent( *, allow_invalid_step_name_regex: bool = False, ) -> list[APIControl]: - """Return API Control models for all configured controls associated with the agent's policy. + """Return API Control models for controls associated with the agent. - Traversal: Agent -> Policy -> Controls (direct relationship). - Uses explicit joins over association table to avoid async relationship loading. + Active controls are the de-duplicated union of: + - controls inherited from all assigned policies + - controls directly associated with the agent Note: Invalid ControlDefinition data triggers an APIValidationError. """ - stmt = ( - select(Control) - .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.name == agent_name) + policy_control_ids = ( + select(policy_controls.c.control_id.label("control_id")) + .select_from( + policy_controls.join( + agent_policies, policy_controls.c.policy_id == agent_policies.c.policy_id + ) + ) + .where(agent_policies.c.agent_name == agent_name) + ) + direct_control_ids = select(agent_controls.c.control_id.label("control_id")).where( + agent_controls.c.agent_name == agent_name + ) + control_ids_subquery = union(policy_control_ids, direct_control_ids).subquery() + + stmt = select(Control).join( + control_ids_subquery, Control.id == control_ids_subquery.c.control_id ) result = await db.execute(stmt) diff --git a/server/tests/test_error_handling.py b/server/tests/test_error_handling.py index ce7cace3..5f139431 100644 --- a/server/tests/test_error_handling.py +++ b/server/tests/test_error_handling.py @@ -406,11 +406,21 @@ async def mock_db_for_policy_assignment() -> AsyncGenerator[AsyncSession, None]: mock_controls_result = MagicMock() mock_controls_result.scalars.return_value.unique.return_value.all.return_value = [] + # Mock existing policy associations query + mock_existing_policy_assoc_result = MagicMock() + mock_existing_policy_assoc_result.all.return_value = [] + + # Mock delete/insert association statements + mock_mutation_result = MagicMock() + # Return different results for different queries mock_session.execute = AsyncMock(side_effect=[ mock_agent_result, mock_policy_result, mock_controls_result, + mock_existing_policy_assoc_result, + mock_mutation_result, + mock_mutation_result, ]) yield mock_session diff --git a/server/tests/test_services_controls.py b/server/tests/test_services_controls.py index 797835d2..72cda230 100644 --- a/server/tests/test_services_controls.py +++ b/server/tests/test_services_controls.py @@ -7,7 +7,14 @@ from agent_control_models.errors import ErrorCode from agent_control_server.errors import APIValidationError -from agent_control_server.models import Agent, Control, Policy, policy_controls +from agent_control_server.models import ( + Agent, + Control, + Policy, + agent_controls, + agent_policies, + policy_controls, +) from agent_control_server.services.controls import list_controls_for_agent, list_controls_for_policy from .utils import VALID_CONTROL_PAYLOAD @@ -42,44 +49,52 @@ async def test_list_controls_for_policy_returns_controls(async_db) -> None: @pytest.mark.asyncio async def test_list_controls_for_agent_returns_controls(async_db) -> None: - # Given: an agent assigned to a policy with one control + # Given: an agent associated with one policy control and one direct control policy = Policy(name=f"policy-{uuid.uuid4()}") - control = Control(name=f"control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) + policy_control = Control(name=f"policy-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) + direct_control = Control(name=f"direct-control-{uuid.uuid4()}", data=VALID_CONTROL_PAYLOAD) agent = Agent( name=f"agent-{uuid.uuid4()}", data={}, - policy=policy, ) - async_db.add_all([policy, control, agent]) + async_db.add_all([policy, policy_control, direct_control, agent]) await async_db.flush() await async_db.execute( - insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) + insert(agent_policies).values({"agent_name": agent.name, "policy_id": policy.id}) + ) + await async_db.execute( + insert(policy_controls).values({"policy_id": policy.id, "control_id": policy_control.id}) + ) + await async_db.execute( + insert(agent_controls).values({"agent_name": agent.name, "control_id": direct_control.id}) ) await async_db.commit() # When: listing controls for the agent controls = await list_controls_for_agent(agent.name, async_db) - # Then: the API control is returned with expected fields - assert len(controls) == 1 - assert controls[0].name == control.name - assert controls[0].control.evaluator.name == VALID_CONTROL_PAYLOAD["evaluator"]["name"] + # Then: both policy-derived and direct controls are returned + assert len(controls) == 2 + names = {control.name for control in controls} + assert names == {policy_control.name, direct_control.name} @pytest.mark.asyncio async def test_list_controls_for_agent_corrupted_data_raises(async_db) -> None: - # Given: an agent assigned to a policy with corrupted control data + # Given: an agent associated with a policy containing corrupted control data policy = Policy(name=f"policy-{uuid.uuid4()}") control = Control(name=f"control-{uuid.uuid4()}", data={"bad": "data"}) agent = Agent( name=f"agent-{uuid.uuid4()}", data={}, - policy=policy, ) async_db.add_all([policy, control, agent]) await async_db.flush() + await async_db.execute( + insert(agent_policies).values({"agent_name": agent.name, "policy_id": policy.id}) + ) await async_db.execute( insert(policy_controls).values({"policy_id": policy.id, "control_id": control.id}) ) From 186c3efcba5c19402e98c445b3e79c8449161b64 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 15:57:54 -0800 Subject: [PATCH 23/32] fix: restore multi-policy and direct-control integrations --- examples/agent_control_demo/setup_controls.py | 47 +- examples/crewai/README.md | 2 +- examples/customer_support_agent/run_demo.py | 20 +- .../setup_demo_controls.py | 9 +- examples/deepeval/setup_controls.py | 31 +- examples/langchain/README.md | 2 +- examples/langchain/setup_sql_controls.py | 7 +- examples/steer_action_demo/setup_controls.py | 7 +- sdks/python/src/agent_control/agents.py | 82 +- .../src/agent_control/control_decorators.py | 9 +- sdks/python/src/agent_control/policies.py | 6 +- sdks/python/tests/test_agent_id_validation.py | 160 +- .../overlays/method-names.overlay.yaml | 30 + .../src/generated/funcs/agents-add-control.ts | 191 + .../src/generated/funcs/agents-add-policy.ts | 191 + .../generated/funcs/agents-delete-policy.ts | 17 +- .../generated/funcs/agents-get-policies.ts | 182 + .../src/generated/funcs/agents-get-policy.ts | 14 +- .../generated/funcs/agents-list-controls.ts | 5 +- .../src/generated/funcs/agents-list.ts | 2 +- .../funcs/agents-remove-all-agent-policies.ts | 185 + .../generated/funcs/agents-remove-control.ts | 191 + .../generated/funcs/agents-remove-policy.ts | 194 + .../generated/funcs/agents-update-policy.ts | 18 +- .../src/generated/funcs/controls-delete.ts | 4 +- .../models/agent-controls-response.ts | 2 +- .../src/generated/models/agent-ref.ts | 2 +- .../src/generated/models/agent-summary.ts | 10 +- .../src/generated/models/control-summary.ts | 6 + .../models/delete-control-response.ts | 14 +- .../models/delete-policy-response.ts | 5 +- .../models/get-agent-policies-response.ts | 42 + .../generated/models/get-policy-response.ts | 5 +- sdks/typescript/src/generated/models/index.ts | 2 + .../generated/models/init-agent-response.ts | 2 +- ...nts-agent-name-controls-control-id-post.ts | 46 + ...ents-agent-name-policies-policy-id-post.ts | 46 + ...ntrol-api-v1-controls-control-id-delete.ts | 2 +- ...s-api-v1-agents-agent-name-policies-get.ts | 42 + .../src/generated/models/operations/index.ts | 6 + ...s-agent-name-controls-control-id-delete.ts | 49 + ...ts-agent-name-policies-policy-id-delete.ts | 49 + ...pi-v1-agents-agent-name-policies-delete.ts | 42 + .../models/remove-agent-control-response.ts | 56 + .../generated/models/set-policy-response.ts | 7 +- sdks/typescript/src/generated/sdk/agents.ts | 164 +- sdks/typescript/src/generated/sdk/controls.ts | 4 +- ui/src/core/api/client.ts | 35 +- ui/src/core/api/generated/api-types.ts | 8515 +++++++++-------- ui/src/core/api/types.ts | 26 +- .../query-hooks/use-add-control-to-agent.ts | 86 +- .../hooks/query-hooks/use-delete-control.ts | 35 +- .../controls/use-delete-control-flow.tsx | 16 +- ui/tests/fixtures.ts | 6 + 54 files changed, 6525 insertions(+), 4403 deletions(-) create mode 100644 sdks/typescript/src/generated/funcs/agents-add-control.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-add-policy.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-get-policies.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-remove-control.ts create mode 100644 sdks/typescript/src/generated/funcs/agents-remove-policy.ts create mode 100644 sdks/typescript/src/generated/models/get-agent-policies-response.ts create mode 100644 sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-name-controls-control-id-post.ts create mode 100644 sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-name-policies-policy-id-post.ts create mode 100644 sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-name-policies-get.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-name-controls-control-id-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-name-policies-policy-id-delete.ts create mode 100644 sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-name-policies-delete.ts create mode 100644 sdks/typescript/src/generated/models/remove-agent-control-response.ts diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index 658d0dda..957a4844 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -244,7 +244,7 @@ async def assign_policy_to_agent( try: response = await client.http_client.post( - f"/api/v1/agents/{agent_name}/policy/{policy_id}" + f"/api/v1/agents/{agent_name}/policies/{policy_id}" ) response.raise_for_status() data = response.json() @@ -379,27 +379,23 @@ async def verify_full_chain(client: AgentControlClient, agent_name: str) -> None except Exception as e: print(f" Error: {e}") - # 2. Get agent's policy - print("\n2. Agent's Policy:") + # 2. Get agent policy associations + print("\n2. Agent Policy Associations:") try: - 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 - else: + resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/policies") + resp.raise_for_status() + policy_ids = resp.json().get("policy_ids", []) + policy_id = policy_ids[0] if policy_ids else None + print(f" Policy IDs: {policy_ids}") + + if policy_id is not None: + # 3. Get policy's controls + print("\n3. Policy's Controls:") + resp = await client.http_client.get(f"/api/v1/policies/{policy_id}/controls") resp.raise_for_status() - policy_data = resp.json() - policy_id = policy_data.get("policy_id") - print(f" Policy ID: {policy_id}") - - if policy_id: - # 3. Get policy's controls - print("\n3. Policy's Controls:") - resp = await client.http_client.get(f"/api/v1/policies/{policy_id}/controls") - resp.raise_for_status() - ctrl_data = resp.json() - control_ids = ctrl_data.get("control_ids", []) - print(f" Control IDs: {control_ids}") + ctrl_data = resp.json() + control_ids = ctrl_data.get("control_ids", []) + print(f" Control IDs: {control_ids}") except Exception as e: print(f" Error: {e}") policy_id = None @@ -495,15 +491,16 @@ async def main(): print("\n Verifying agent policy assignment...") try: resp = await client.http_client.get( - f"/api/v1/agents/{agent_name}/policy" + f"/api/v1/agents/{agent_name}/policies" ) resp.raise_for_status() - agent_policy = resp.json() - assigned_policy_id = agent_policy.get("policy_id") - if assigned_policy_id == policy_id: + assigned_policy_ids = resp.json().get("policy_ids", []) + if policy_id in assigned_policy_ids: print(f" ✓ Agent correctly assigned to policy {policy_id}") else: - print(f" ⚠️ Agent assigned to policy {assigned_policy_id}, expected {policy_id}") + print( + f" ⚠️ Agent policy IDs are {assigned_policy_ids}, expected to include {policy_id}" + ) except Exception as e: print(f" ✗ Failed to verify agent policy: {e}") diff --git a/examples/crewai/README.md b/examples/crewai/README.md index 6540cf89..dea99856 100644 --- a/examples/crewai/README.md +++ b/examples/crewai/README.md @@ -475,7 +475,7 @@ Return result or raise ControlViolationError ## Files - `content_agent_protection.py` - Main CrewAI crew with @control() -- `setup_content_controls.py` - One-time setup for controls/policy +- `setup_content_controls.py` - One-time setup for controls/policies - `pyproject.toml` - Dependencies - `README.md` - This file diff --git a/examples/customer_support_agent/run_demo.py b/examples/customer_support_agent/run_demo.py index 34ad71ef..a80327a3 100644 --- a/examples/customer_support_agent/run_demo.py +++ b/examples/customer_support_agent/run_demo.py @@ -59,7 +59,7 @@ async def reset_agent(): - """Reset the agent by removing its policy (which disconnects all controls).""" + """Reset the agent by removing all policy associations.""" agent_name = AGENT_ID server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -81,22 +81,22 @@ async def reset_agent(): print(f"Error checking agent: {e}") return - # Remove policy from agent (disconnects all controls) + # Remove all policies from agent (disconnects policy-derived controls) try: - await agents.remove_agent_policy(client, agent_name) - logger.info("Successfully removed policy from agent") - print("Removed policy from agent (all controls disconnected).") + await agents.remove_all_agent_policies(client, agent_name) + logger.info("Successfully removed all policies from agent") + print("Removed all policies from agent.") except Exception as e: if "404" in str(e): - logger.info("Agent has no policy attached") - print("Agent has no policy - already clean.") + logger.info("Agent not found while removing policies") + print("Agent not found - nothing to reset.") return - logger.error(f"Error removing policy: {e}") - print(f"Error removing policy: {e}") + logger.error(f"Error removing policies: {e}") + print(f"Error removing policies: {e}") return print() - print("Reset complete. The agent now has no controls.") + print("Reset complete. The agent now has no policy-derived controls.") print("Run the demo again and add controls via the UI to test.") diff --git a/examples/customer_support_agent/setup_demo_controls.py b/examples/customer_support_agent/setup_demo_controls.py index bce590fe..01746e68 100644 --- a/examples/customer_support_agent/setup_demo_controls.py +++ b/examples/customer_support_agent/setup_demo_controls.py @@ -256,12 +256,13 @@ async def setup_demo(quiet: bool = False): policy_name = f"policy-{AGENT_ID}" policy_id = None - # Check if agent already has a policy + # Check if agent already has policies try: - policy_info = await agents.get_agent_policy(client, agent_name) - policy_id = policy_info.get("policy_id") + policy_info = await agents.get_agent_policies(client, agent_name) + policy_ids = policy_info.get("policy_ids", []) + policy_id = policy_ids[0] if policy_ids else None except Exception: - policy_id = None # No policy yet + policy_id = None # No policy associations yet # Create policy if needed if not policy_id: diff --git a/examples/deepeval/setup_controls.py b/examples/deepeval/setup_controls.py index f3178cb5..97bf5dd2 100755 --- a/examples/deepeval/setup_controls.py +++ b/examples/deepeval/setup_controls.py @@ -19,8 +19,6 @@ import asyncio import os import sys -import uuid - import httpx # Add the current directory to the path so we can import the evaluator @@ -148,12 +146,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_name = str(uuid.uuid5(uuid.NAMESPACE_DNS, AGENT_ID)) + agent_name = AGENT_ID print(f"Setting up agent: {AGENT_NAME}") print(f"Agent ID: {AGENT_ID}") - print(f"Agent UUID: {agent_name}") + print(f"Agent Name: {agent_name}") print(f"Server URL: {SERVER_URL}") print() @@ -176,10 +173,10 @@ async def setup_demo(quiet: bool = False): json={ "agent": { "agent_name": agent_name, - "agent_name": AGENT_NAME, "agent_description": AGENT_DESCRIPTION, + "agent_version": "1.0.0", }, - "tools": [], + "steps": [], }, ) resp.raise_for_status() @@ -194,14 +191,16 @@ async def setup_demo(quiet: bool = False): policy_name = f"policy-{AGENT_ID}" policy_id = None - # Check if agent already has a policy + # Check if agent already has associated policies try: - resp = await client.get(f"/api/v1/agents/{agent_name}/policy") + resp = await client.get(f"/api/v1/agents/{agent_name}/policies") if resp.status_code == 200: - policy_id = resp.json().get("policy_id") - print(f"✓ Found existing policy: {policy_id}") + policy_ids = resp.json().get("policy_ids", []) + if policy_ids: + policy_id = policy_ids[0] + print(f"✓ Found existing policy: {policy_id}") except httpx.HTTPError: - pass # No policy yet + pass # No policies yet # Create policy if needed if not policy_id: @@ -223,10 +222,12 @@ async def setup_demo(quiet: bool = False): policy_id = resp.json()["policy_id"] print(f"✓ Created policy: {policy_name}") - # Assign policy to agent - resp = await client.post(f"/api/v1/agents/{agent_name}/policy/{policy_id}") + # Associate policy with agent + resp = await client.post( + f"/api/v1/agents/{agent_name}/policies/{policy_id}" + ) resp.raise_for_status() - print(f"✓ Assigned policy to agent") + print("✓ Associated policy with agent") except httpx.HTTPError as e: print(f"❌ Error setting up policy: {e}") return False diff --git a/examples/langchain/README.md b/examples/langchain/README.md index d9169459..20513508 100644 --- a/examples/langchain/README.md +++ b/examples/langchain/README.md @@ -192,7 +192,7 @@ Raise ControlViolationError (DENY) or Execute (ALLOW) ## Files - `sql_agent_protection.py` - Main SQL agent with `@control()` decorator -- `setup_sql_controls.py` - One-time setup script for controls/policy +- `setup_sql_controls.py` - One-time setup script for controls/policies - `pyproject.toml` - Dependencies and configuration - `README.md` - This file diff --git a/examples/langchain/setup_sql_controls.py b/examples/langchain/setup_sql_controls.py index 28512907..7feffd33 100644 --- a/examples/langchain/setup_sql_controls.py +++ b/examples/langchain/setup_sql_controls.py @@ -139,10 +139,11 @@ 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_name)) - policy_id = policy_info.get("policy_id") + policy_info = await agents.get_agent_policies(client, str(agent_name)) + policy_ids = policy_info.get("policy_ids", []) + policy_id = policy_ids[0] if policy_ids else None if policy_id is None: - raise ValueError("No policy assigned to agent.") + raise ValueError("No policies assigned to agent.") print(f"ℹ️ Using agent's existing policy (ID: {policy_id})") except Exception: # Create a new policy name to avoid conflicts and keep script idempotent. diff --git a/examples/steer_action_demo/setup_controls.py b/examples/steer_action_demo/setup_controls.py index c57b5d4a..3d4d2781 100644 --- a/examples/steer_action_demo/setup_controls.py +++ b/examples/steer_action_demo/setup_controls.py @@ -251,9 +251,10 @@ async def setup_banking_controls(): if "409" in str(e): print(f" ℹ️ Policy 'banking-transaction-policy' already exists") try: - policy_info = await agents.get_agent_policy(client, agent.agent_name) - policy_id = policy_info.get("policy_id") - if policy_id: + policy_info = await agents.get_agent_policies(client, agent.agent_name) + policy_ids = policy_info.get("policy_ids", []) + policy_id = policy_ids[0] if policy_ids else None + if policy_id is not None: print(f" ℹ️ Using agent's existing policy (ID: {policy_id})") else: raise ValueError("No policy assigned") diff --git a/sdks/python/src/agent_control/agents.py b/sdks/python/src/agent_control/agents.py index b14e69a8..3d63cc5e 100644 --- a/sdks/python/src/agent_control/agents.py +++ b/sdks/python/src/agent_control/agents.py @@ -54,11 +54,61 @@ async def list_agents( return cast(dict[str, Any], response.json()) +async def get_agent_policies( + client: AgentControlClient, + agent_name: str, +) -> dict[str, Any]: + """List policy IDs associated with an agent.""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.get(f"/api/v1/agents/{normalized_name}/policies") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def add_agent_policy( + client: AgentControlClient, + agent_name: str, + policy_id: int, +) -> dict[str, Any]: + """Associate a policy with an agent (additive, idempotent).""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.post( + f"/api/v1/agents/{normalized_name}/policies/{policy_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_policy_association( + client: AgentControlClient, + agent_name: str, + policy_id: int, +) -> dict[str, Any]: + """Remove one policy association from an agent (idempotent).""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.delete( + f"/api/v1/agents/{normalized_name}/policies/{policy_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_all_agent_policies( + client: AgentControlClient, + agent_name: str, +) -> dict[str, Any]: + """Remove all policy associations from an agent.""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.delete(f"/api/v1/agents/{normalized_name}/policies") + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def get_agent_policy( client: AgentControlClient, agent_name: str, ) -> dict[str, Any]: - """Get the policy assigned to an agent.""" + """Get the primary policy assigned to an agent (compatibility endpoint).""" normalized_name = ensure_agent_name(agent_name) response = await client.http_client.get(f"/api/v1/agents/{normalized_name}/policy") response.raise_for_status() @@ -69,13 +119,41 @@ async def remove_agent_policy( client: AgentControlClient, agent_name: str, ) -> dict[str, Any]: - """Remove the policy assignment from an agent.""" + """Remove all policy associations via singular compatibility endpoint.""" 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()) +async def add_agent_control( + client: AgentControlClient, + agent_name: str, + control_id: int, +) -> dict[str, Any]: + """Associate a control directly with an agent (idempotent).""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.post( + f"/api/v1/agents/{normalized_name}/controls/{control_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + +async def remove_agent_control( + client: AgentControlClient, + agent_name: str, + control_id: int, +) -> dict[str, Any]: + """Remove a direct control association from an agent (idempotent).""" + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.delete( + f"/api/v1/agents/{normalized_name}/controls/{control_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + + async def list_agent_controls( client: AgentControlClient, agent_name: str, diff --git a/sdks/python/src/agent_control/control_decorators.py b/sdks/python/src/agent_control/control_decorators.py index 24789513..931d1c88 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -783,11 +783,12 @@ async def handle_user_input(user_message: str) -> str: PUT /api/v1/policies {"name": "safety-policy"} POST /api/v1/policies/{policy_id}/controls/{control_id} - 3. Assign policy to agent: - POST /api/v1/agents/{agent_name}/policy/{policy_id} + 3. Associate policy and/or controls with an agent: + POST /api/v1/agents/{agent_name}/policies/{policy_id} + POST /api/v1/agents/{agent_name}/controls/{control_id} """ - # The policy parameter is for documentation only - the server uses - # the agent's assigned policy automatically + # The policy parameter is for documentation only - the server evaluates + # controls associated with the agent via policy and direct links. _ = policy def decorator(func: F) -> F: diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index aef75bbf..0f5f913d 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -155,9 +155,9 @@ async def assign_policy_to_agent( policy_id: int ) -> dict[str, Any]: """ - Assign a policy to an agent. + Associate a policy with an agent. - This makes the policy active for the agent. Any existing policy assignment is replaced. + This operation is additive and idempotent. Agents can have multiple policy associations. Args: client: AgentControlClient instance @@ -173,7 +173,7 @@ async def assign_policy_to_agent( """ agent_name_str = ensure_agent_name(agent_name) response = await client.http_client.post( - f"/api/v1/agents/{agent_name_str}/policy/{policy_id}" + f"/api/v1/agents/{agent_name_str}/policies/{policy_id}" ) response.raise_for_status() return cast(dict[str, Any], response.json()) diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py index 20277e22..6c5f0c2f 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -1,8 +1,8 @@ """SDK agent name validation behavior tests.""" from unittest.mock import AsyncMock, MagicMock -import pytest +import pytest from agent_control import agents, policies @@ -38,6 +38,18 @@ async def test_get_agent_policy_rejects_invalid_agent_name() -> None: client.http_client.get.assert_not_called() +@pytest.mark.asyncio +async def test_get_agent_policies_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.get_agent_policies(client, "short") + + client.http_client.get.assert_not_called() + + @pytest.mark.asyncio async def test_remove_agent_policy_rejects_invalid_agent_name() -> None: client = MagicMock() @@ -50,6 +62,66 @@ async def test_remove_agent_policy_rejects_invalid_agent_name() -> None: client.http_client.delete.assert_not_called() +@pytest.mark.asyncio +async def test_remove_all_agent_policies_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.remove_all_agent_policies(client, "short") + + client.http_client.delete.assert_not_called() + + +@pytest.mark.asyncio +async def test_add_agent_policy_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.add_agent_policy(client, "short", policy_id=1) + + client.http_client.post.assert_not_called() + + +@pytest.mark.asyncio +async def test_remove_agent_policy_association_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.remove_agent_policy_association(client, "short", policy_id=1) + + client.http_client.delete.assert_not_called() + + +@pytest.mark.asyncio +async def test_add_agent_control_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.add_agent_control(client, "short", control_id=1) + + client.http_client.post.assert_not_called() + + +@pytest.mark.asyncio +async def test_remove_agent_control_rejects_invalid_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock() + + with pytest.raises(ValueError, match="at least 10 characters"): + await agents.remove_agent_control(client, "short", control_id=1) + + client.http_client.delete.assert_not_called() + + @pytest.mark.asyncio async def test_list_agents_normalizes_cursor() -> None: client = MagicMock() @@ -82,8 +154,7 @@ async def test_get_agent_normalizes_agent_name() -> None: client.http_client = MagicMock() client.http_client.get = AsyncMock(return_value=DummyResponse()) - agent_name = "Agent-Example_01" - await agents.get_agent(client, agent_name) + await agents.get_agent(client, "Agent-Example_01") client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01") @@ -99,6 +170,17 @@ async def test_get_agent_policy_normalizes_agent_name() -> None: client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy") +@pytest.mark.asyncio +async def test_get_agent_policies_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.get = AsyncMock(return_value=DummyResponse()) + + await agents.get_agent_policies(client, "Agent-Example_01") + + client.http_client.get.assert_awaited_once_with("/api/v1/agents/agent-example_01/policies") + + @pytest.mark.asyncio async def test_remove_agent_policy_normalizes_agent_name() -> None: client = MagicMock() @@ -108,3 +190,75 @@ async def test_remove_agent_policy_normalizes_agent_name() -> None: await agents.remove_agent_policy(client, "Agent-Example_01") client.http_client.delete.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy") + + +@pytest.mark.asyncio +async def test_remove_all_agent_policies_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock(return_value=DummyResponse()) + + await agents.remove_all_agent_policies(client, "Agent-Example_01") + + client.http_client.delete.assert_awaited_once_with( + "/api/v1/agents/agent-example_01/policies" + ) + + +@pytest.mark.asyncio +async def test_add_agent_policy_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock(return_value=DummyResponse()) + + await agents.add_agent_policy(client, "Agent-Example_01", policy_id=3) + + client.http_client.post.assert_awaited_once_with("/api/v1/agents/agent-example_01/policies/3") + + +@pytest.mark.asyncio +async def test_remove_agent_policy_association_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock(return_value=DummyResponse()) + + await agents.remove_agent_policy_association(client, "Agent-Example_01", policy_id=3) + + client.http_client.delete.assert_awaited_once_with( + "/api/v1/agents/agent-example_01/policies/3" + ) + + +@pytest.mark.asyncio +async def test_add_agent_control_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock(return_value=DummyResponse()) + + await agents.add_agent_control(client, "Agent-Example_01", control_id=9) + + client.http_client.post.assert_awaited_once_with("/api/v1/agents/agent-example_01/controls/9") + + +@pytest.mark.asyncio +async def test_remove_agent_control_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.delete = AsyncMock(return_value=DummyResponse()) + + await agents.remove_agent_control(client, "Agent-Example_01", control_id=9) + + client.http_client.delete.assert_awaited_once_with( + "/api/v1/agents/agent-example_01/controls/9" + ) + + +@pytest.mark.asyncio +async def test_assign_policy_normalizes_agent_name() -> None: + client = MagicMock() + client.http_client = MagicMock() + client.http_client.post = AsyncMock(return_value=DummyResponse()) + + await policies.assign_policy_to_agent(client, "Agent-Example_01", policy_id=11) + + client.http_client.post.assert_awaited_once_with("/api/v1/agents/agent-example_01/policies/11") diff --git a/sdks/typescript/overlays/method-names.overlay.yaml b/sdks/typescript/overlays/method-names.overlay.yaml index fd4927f7..3093d3f8 100644 --- a/sdks/typescript/overlays/method-names.overlay.yaml +++ b/sdks/typescript/overlays/method-names.overlay.yaml @@ -30,6 +30,16 @@ actions: x-speakeasy-group: agents x-speakeasy-name-override: listControls + - target: $["paths"]["/api/v1/agents/{agent_name}/controls/{control_id}"]["post"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: addControl + + - target: $["paths"]["/api/v1/agents/{agent_name}/controls/{control_id}"]["delete"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: removeControl + - target: $["paths"]["/api/v1/agents/{agent_name}/evaluators"]["get"] update: x-speakeasy-group: agents @@ -40,6 +50,26 @@ actions: x-speakeasy-group: agents x-speakeasy-name-override: getEvaluator + - target: $["paths"]["/api/v1/agents/{agent_name}/policies"]["get"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: getPolicies + + - target: $["paths"]["/api/v1/agents/{agent_name}/policies"]["delete"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: removeAllAgentPolicies + + - target: $["paths"]["/api/v1/agents/{agent_name}/policies/{policy_id}"]["post"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: addPolicy + + - target: $["paths"]["/api/v1/agents/{agent_name}/policies/{policy_id}"]["delete"] + update: + x-speakeasy-group: agents + x-speakeasy-name-override: removePolicy + - target: $["paths"]["/api/v1/agents/{agent_name}/policy"]["get"] update: x-speakeasy-group: agents diff --git a/sdks/typescript/src/generated/funcs/agents-add-control.ts b/sdks/typescript/src/generated/funcs/agents-add-control.ts new file mode 100644 index 00000000..08a7553f --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-add-control.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Associate control directly with agent + * + * @remarks + * Associate a control directly with an agent (idempotent). + */ +export function agentsAddControl( + client: AgentControlSDKCore, + request: + operations.AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + control_id: encodeSimple("control_id", payload.control_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/controls/{control_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "add_agent_control_api_v1_agents__agent_name__controls__control_id__post", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "POST", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-add-policy.ts b/sdks/typescript/src/generated/funcs/agents-add-policy.ts new file mode 100644 index 00000000..76e7dfb5 --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-add-policy.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Associate policy with agent + * + * @remarks + * Associate a policy with an agent (idempotent). + */ +export function agentsAddPolicy( + client: AgentControlSDKCore, + request: + operations.AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + policy_id: encodeSimple("policy_id", payload.policy_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/policies/{policy_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "POST", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts index 2e8a18b6..a87b1627 100644 --- a/sdks/typescript/src/generated/funcs/agents-delete-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-delete-policy.ts @@ -28,23 +28,10 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Remove agent's policy assignment + * Remove agent's policy assignment (compatibility) * * @remarks - * Remove the policy assignment from an agent. - * - * The agent will no longer have any protection controls active. - * - * Args: - * agent_name: Agent identifier - * 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 + * Compatibility endpoint that removes all policy associations. */ export function agentsDeletePolicy( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/funcs/agents-get-policies.ts b/sdks/typescript/src/generated/funcs/agents-get-policies.ts new file mode 100644 index 00000000..6c1c9168 --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-get-policies.ts @@ -0,0 +1,182 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * List policies associated with agent + * + * @remarks + * List policy IDs associated with an agent. + */ +export function agentsGetPolicies( + client: AgentControlSDKCore, + request: operations.GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.GetAgentPoliciesResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: operations.GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.GetAgentPoliciesResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/policies")(pathParams); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: "get_agent_policies_api_v1_agents__agent_name__policies_get", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "GET", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.GetAgentPoliciesResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.GetAgentPoliciesResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-get-policy.ts b/sdks/typescript/src/generated/funcs/agents-get-policy.ts index cbe953f3..df3755d8 100644 --- a/sdks/typescript/src/generated/funcs/agents-get-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-get-policy.ts @@ -28,20 +28,10 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Get agent's assigned policy + * Get agent's assigned policy (compatibility) * * @remarks - * Retrieve the policy currently assigned to an agent. - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * GetPolicyResponse with policy ID - * - * Raises: - * HTTPException 404: Agent not found or agent has no policy assigned + * Compatibility endpoint that returns the first associated policy. */ export function agentsGetPolicy( 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 b44c3402..d0776636 100644 --- a/sdks/typescript/src/generated/funcs/agents-list-controls.ts +++ b/sdks/typescript/src/generated/funcs/agents-list-controls.ts @@ -33,15 +33,14 @@ import { Result } from "../types/fp.js"; * @remarks * List all protection controls active for an agent. * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. + * Controls include the union of policy-derived and directly associated controls. * * Args: * agent_name: Agent identifier * db: Database session (injected) * * Returns: - * AgentControlsResponse with list of controls (empty if no policy) + * AgentControlsResponse with list of active controls * * Raises: * HTTPException 404: Agent not found diff --git a/sdks/typescript/src/generated/funcs/agents-list.ts b/sdks/typescript/src/generated/funcs/agents-list.ts index 82e21470..5dcbb7c6 100644 --- a/sdks/typescript/src/generated/funcs/agents-list.ts +++ b/sdks/typescript/src/generated/funcs/agents-list.ts @@ -33,7 +33,7 @@ import { Result } from "../types/fp.js"; * @remarks * List all registered agents with cursor-based pagination. * - * Returns a summary of each agent including identifier, policy assignment, + * Returns a summary of each agent including identifier, policy associations, * and counts of registered steps and evaluators. * * Args: diff --git a/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts b/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts new file mode 100644 index 00000000..01234e18 --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-remove-all-agent-policies.ts @@ -0,0 +1,185 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Remove all policy associations from agent + * + * @remarks + * Remove all policy associations from an agent. + */ +export function agentsRemoveAllAgentPolicies( + client: AgentControlSDKCore, + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/policies")(pathParams); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "remove_all_agent_policies_api_v1_agents__agent_name__policies_delete", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "DELETE", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-remove-control.ts b/sdks/typescript/src/generated/funcs/agents-remove-control.ts new file mode 100644 index 00000000..c23e3bec --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-remove-control.ts @@ -0,0 +1,191 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Remove direct control association from agent + * + * @remarks + * Remove a direct control association from an agent (idempotent). + */ +export function agentsRemoveControl( + client: AgentControlSDKCore, + request: + operations.RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.RemoveAgentControlResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.RemoveAgentControlResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + control_id: encodeSimple("control_id", payload.control_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/controls/{control_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "DELETE", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.RemoveAgentControlResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.RemoveAgentControlResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-remove-policy.ts b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts new file mode 100644 index 00000000..b275052a --- /dev/null +++ b/sdks/typescript/src/generated/funcs/agents-remove-policy.ts @@ -0,0 +1,194 @@ +/* + * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. + */ + +import * as z from "zod/v4-mini"; +import { AgentControlSDKCore } from "../core.js"; +import { encodeSimple } from "../lib/encodings.js"; +import * as M from "../lib/matchers.js"; +import { compactMap } from "../lib/primitives.js"; +import { safeParse } from "../lib/schemas.js"; +import { RequestOptions } from "../lib/sdks.js"; +import { extractSecurity, resolveGlobalSecurity } from "../lib/security.js"; +import { pathToFunc } from "../lib/url.js"; +import { AgentControlSDKError } from "../models/errors/agent-control-sdk-error.js"; +import { + ConnectionError, + InvalidRequestError, + RequestAbortedError, + RequestTimeoutError, + UnexpectedClientError, +} from "../models/errors/http-client-errors.js"; +import * as errors from "../models/errors/index.js"; +import { ResponseValidationError } from "../models/errors/response-validation-error.js"; +import { SDKValidationError } from "../models/errors/sdk-validation-error.js"; +import * as models from "../models/index.js"; +import * as operations from "../models/operations/index.js"; +import { APICall, APIPromise } from "../types/async.js"; +import { Result } from "../types/fp.js"; + +/** + * Remove policy association from agent + * + * @remarks + * Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. + */ +export function agentsRemovePolicy( + client: AgentControlSDKCore, + request: + operations.RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest, + options?: RequestOptions, +): APIPromise< + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + > +> { + return new APIPromise($do( + client, + request, + options, + )); +} + +async function $do( + client: AgentControlSDKCore, + request: + operations.RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest, + options?: RequestOptions, +): Promise< + [ + Result< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >, + APICall, + ] +> { + const parsed = safeParse( + request, + (value) => + z.parse( + operations + .RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest$outboundSchema, + value, + ), + "Input validation failed", + ); + if (!parsed.ok) { + return [parsed, { status: "invalid" }]; + } + const payload = parsed.value; + const body = null; + + const pathParams = { + agent_name: encodeSimple("agent_name", payload.agent_name, { + explode: false, + charEncoding: "percent", + }), + policy_id: encodeSimple("policy_id", payload.policy_id, { + explode: false, + charEncoding: "percent", + }), + }; + + const path = pathToFunc("/api/v1/agents/{agent_name}/policies/{policy_id}")( + pathParams, + ); + + const headers = new Headers(compactMap({ + Accept: "application/json", + })); + + const secConfig = await extractSecurity(client._options.apiKeyHeader); + const securityInput = secConfig == null ? {} : { apiKeyHeader: secConfig }; + const requestSecurity = resolveGlobalSecurity(securityInput); + + const context = { + options: client._options, + baseURL: options?.serverURL ?? client._baseURL ?? "", + operationID: + "remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete", + oAuth2Scopes: null, + + resolvedSecurity: requestSecurity, + + securitySource: client._options.apiKeyHeader, + retryConfig: options?.retries + || client._options.retryConfig + || { strategy: "none" }, + retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], + }; + + const requestRes = client._createRequest(context, { + security: requestSecurity, + method: "DELETE", + baseURL: options?.serverURL, + path: path, + headers: headers, + body: body, + userAgent: client._options.userAgent, + timeoutMs: options?.timeoutMs || client._options.timeoutMs || -1, + }, options); + if (!requestRes.ok) { + return [requestRes, { status: "invalid" }]; + } + const req = requestRes.value; + + const doResult = await client._do(req, { + context, + errorCodes: ["422", "4XX", "5XX"], + retryConfig: context.retryConfig, + retryCodes: context.retryCodes, + }); + if (!doResult.ok) { + return [doResult, { status: "request-error", request: req }]; + } + const response = doResult.value; + + const responseFields = { + HttpMeta: { Response: response, Request: req }, + }; + + const [result] = await M.match< + models.AssocResponse, + | errors.HTTPValidationError + | AgentControlSDKError + | ResponseValidationError + | ConnectionError + | RequestAbortedError + | RequestTimeoutError + | InvalidRequestError + | UnexpectedClientError + | SDKValidationError + >( + M.json(200, models.AssocResponse$inboundSchema), + M.jsonErr(422, errors.HTTPValidationError$inboundSchema), + M.fail("4XX"), + M.fail("5XX"), + )(response, req, { extraFields: responseFields }); + if (!result.ok) { + return [result, { status: "complete", request: req, response }]; + } + + return [result, { status: "complete", request: req, response }]; +} diff --git a/sdks/typescript/src/generated/funcs/agents-update-policy.ts b/sdks/typescript/src/generated/funcs/agents-update-policy.ts index 7626a798..f120912b 100644 --- a/sdks/typescript/src/generated/funcs/agents-update-policy.ts +++ b/sdks/typescript/src/generated/funcs/agents-update-policy.ts @@ -28,24 +28,10 @@ import { APICall, APIPromise } from "../types/async.js"; import { Result } from "../types/fp.js"; /** - * Assign policy to agent + * Assign policy to agent (compatibility) * * @remarks - * Assign a policy to an agent, replacing any existing policy assignment. - * - * The agent will immediately inherit all controls from the assigned policy. - * - * Args: - * agent_name: Agent identifier - * 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 + * Compatibility endpoint that replaces all policy associations with one policy. */ export function agentsUpdatePolicy( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/funcs/controls-delete.ts b/sdks/typescript/src/generated/funcs/controls-delete.ts index c3b65d39..ea973a24 100644 --- a/sdks/typescript/src/generated/funcs/controls-delete.ts +++ b/sdks/typescript/src/generated/funcs/controls-delete.ts @@ -33,7 +33,7 @@ import { Result } from "../types/fp.js"; * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy. + * By default, deletion fails if the control is associated with any policy or agent. * Use force=true to automatically dissociate and delete. * * Args: @@ -42,7 +42,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and list of dissociated policies + * DeleteControlResponse with success flag and dissociation details * * Raises: * HTTPException 404: Control not found diff --git a/sdks/typescript/src/generated/models/agent-controls-response.ts b/sdks/typescript/src/generated/models/agent-controls-response.ts index 0668702c..22a591ee 100644 --- a/sdks/typescript/src/generated/models/agent-controls-response.ts +++ b/sdks/typescript/src/generated/models/agent-controls-response.ts @@ -10,7 +10,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; export type AgentControlsResponse = { /** - * List of controls associated with the agent via its policy + * List of active controls associated with the agent */ controls: Array; }; diff --git a/sdks/typescript/src/generated/models/agent-ref.ts b/sdks/typescript/src/generated/models/agent-ref.ts index e24952df..676ac11a 100644 --- a/sdks/typescript/src/generated/models/agent-ref.ts +++ b/sdks/typescript/src/generated/models/agent-ref.ts @@ -14,7 +14,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type AgentRef = { /** - * Agent identifier + * Agent name */ agentName: string; }; diff --git a/sdks/typescript/src/generated/models/agent-summary.ts b/sdks/typescript/src/generated/models/agent-summary.ts index a2cccd70..7e7a5472 100644 --- a/sdks/typescript/src/generated/models/agent-summary.ts +++ b/sdks/typescript/src/generated/models/agent-summary.ts @@ -14,7 +14,7 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type AgentSummary = { /** - * Number of active controls from agent's policy + * Number of active controls for this agent */ activeControlsCount: number; /** @@ -30,9 +30,13 @@ export type AgentSummary = { */ evaluatorCount: number; /** - * ID of assigned policy, if any + * Deprecated: first associated policy ID, if any */ policyId?: number | null | undefined; + /** + * IDs of policies associated with the agent + */ + policyIds?: Array | undefined; /** * Number of steps registered with the agent */ @@ -48,6 +52,7 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = created_at: z.optional(z.nullable(types.string())), evaluator_count: z._default(types.number(), 0), policy_id: z.optional(z.nullable(types.number())), + policy_ids: types.optional(z.array(types.number())), step_count: z._default(types.number(), 0), }), z.transform((v) => { @@ -57,6 +62,7 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = "created_at": "createdAt", "evaluator_count": "evaluatorCount", "policy_id": "policyId", + "policy_ids": "policyIds", "step_count": "stepCount", }); }), diff --git a/sdks/typescript/src/generated/models/control-summary.ts b/sdks/typescript/src/generated/models/control-summary.ts index 9e0356a5..7e89a07f 100644 --- a/sdks/typescript/src/generated/models/control-summary.ts +++ b/sdks/typescript/src/generated/models/control-summary.ts @@ -50,6 +50,10 @@ export type ControlSummary = { * Agent using this control */ usedByAgent?: AgentRef | null | undefined; + /** + * Number of unique agents using this control + */ + usedByAgentsCount: number; }; /** @internal */ @@ -67,11 +71,13 @@ export const ControlSummary$inboundSchema: z.ZodMiniType< step_types: z.optional(z.nullable(z.array(types.string()))), tags: types.optional(z.array(types.string())), used_by_agent: z.optional(z.nullable(AgentRef$inboundSchema)), + used_by_agents_count: z._default(types.number(), 0), }), z.transform((v) => { return remap$(v, { "step_types": "stepTypes", "used_by_agent": "usedByAgent", + "used_by_agents_count": "usedByAgentsCount", }); }), ); diff --git a/sdks/typescript/src/generated/models/delete-control-response.ts b/sdks/typescript/src/generated/models/delete-control-response.ts index 623abd95..40f6c252 100644 --- a/sdks/typescript/src/generated/models/delete-control-response.ts +++ b/sdks/typescript/src/generated/models/delete-control-response.ts @@ -14,9 +14,17 @@ import { SDKValidationError } from "./errors/sdk-validation-error.js"; */ export type DeleteControlResponse = { /** - * Policy IDs the control was removed from before deletion + * Deprecated: policy IDs the control was removed from before deletion */ dissociatedFrom?: Array | undefined; + /** + * Agent names the control was removed from before deletion + */ + dissociatedFromAgents?: Array | undefined; + /** + * Policy IDs the control was removed from before deletion + */ + dissociatedFromPolicies?: Array | undefined; /** * Whether the control was deleted */ @@ -30,11 +38,15 @@ export const DeleteControlResponse$inboundSchema: z.ZodMiniType< > = z.pipe( z.object({ dissociated_from: types.optional(z.array(types.number())), + dissociated_from_agents: types.optional(z.array(types.string())), + dissociated_from_policies: types.optional(z.array(types.number())), success: types.boolean(), }), z.transform((v) => { return remap$(v, { "dissociated_from": "dissociatedFrom", + "dissociated_from_agents": "dissociatedFromAgents", + "dissociated_from_policies": "dissociatedFromPolicies", }); }), ); diff --git a/sdks/typescript/src/generated/models/delete-policy-response.ts b/sdks/typescript/src/generated/models/delete-policy-response.ts index d37ef18e..760045ca 100644 --- a/sdks/typescript/src/generated/models/delete-policy-response.ts +++ b/sdks/typescript/src/generated/models/delete-policy-response.ts @@ -8,9 +8,12 @@ import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; +/** + * Compatibility response for singular policy deletion endpoint. + */ export type DeletePolicyResponse = { /** - * Whether the policy was successfully removed + * Whether the request succeeded */ success: boolean; }; diff --git a/sdks/typescript/src/generated/models/get-agent-policies-response.ts b/sdks/typescript/src/generated/models/get-agent-policies-response.ts new file mode 100644 index 00000000..3e9413e0 --- /dev/null +++ b/sdks/typescript/src/generated/models/get-agent-policies-response.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"; +import { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import * as types from "../types/primitives.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; + +export type GetAgentPoliciesResponse = { + /** + * IDs of policies associated with the agent + */ + policyIds?: Array | undefined; +}; + +/** @internal */ +export const GetAgentPoliciesResponse$inboundSchema: z.ZodMiniType< + GetAgentPoliciesResponse, + unknown +> = z.pipe( + z.object({ + policy_ids: types.optional(z.array(types.number())), + }), + z.transform((v) => { + return remap$(v, { + "policy_ids": "policyIds", + }); + }), +); + +export function getAgentPoliciesResponseFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => GetAgentPoliciesResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'GetAgentPoliciesResponse' from JSON`, + ); +} diff --git a/sdks/typescript/src/generated/models/get-policy-response.ts b/sdks/typescript/src/generated/models/get-policy-response.ts index fd8437df..83e05a8a 100644 --- a/sdks/typescript/src/generated/models/get-policy-response.ts +++ b/sdks/typescript/src/generated/models/get-policy-response.ts @@ -9,9 +9,12 @@ import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; +/** + * Compatibility response for singular policy retrieval endpoint. + */ export type GetPolicyResponse = { /** - * Identifier of the policy assigned to the agent + * Associated policy ID */ policyId: number; }; diff --git a/sdks/typescript/src/generated/models/index.ts b/sdks/typescript/src/generated/models/index.ts index af193ed3..29a27023 100644 --- a/sdks/typescript/src/generated/models/index.ts +++ b/sdks/typescript/src/generated/models/index.ts @@ -39,6 +39,7 @@ export * from "./evaluator-schema.js"; export * from "./evaluator-spec.js"; export * from "./event-query-request.js"; export * from "./event-query-response.js"; +export * from "./get-agent-policies-response.js"; export * from "./get-agent-response.js"; export * from "./get-control-data-response.js"; export * from "./get-control-response.js"; @@ -58,6 +59,7 @@ export * from "./patch-agent-request.js"; export * from "./patch-agent-response.js"; export * from "./patch-control-request.js"; export * from "./patch-control-response.js"; +export * from "./remove-agent-control-response.js"; export * from "./security.js"; export * from "./set-control-data-request.js"; export * from "./set-control-data-response.js"; diff --git a/sdks/typescript/src/generated/models/init-agent-response.ts b/sdks/typescript/src/generated/models/init-agent-response.ts index c2e00b02..76ade4de 100644 --- a/sdks/typescript/src/generated/models/init-agent-response.ts +++ b/sdks/typescript/src/generated/models/init-agent-response.ts @@ -19,7 +19,7 @@ import { */ export type InitAgentResponse = { /** - * Active protection controls for the agent (if policy assigned) + * Active protection controls for the agent */ controls?: Array | undefined; /** diff --git a/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-name-controls-control-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-name-controls-control-id-post.ts new file mode 100644 index 00000000..dec32e6e --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/add-agent-control-api-v1-agents-agent-name-controls-control-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 AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest = { + agentName: string; + controlId: number; +}; + +/** @internal */ +export type AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest$Outbound = + { + agent_name: string; + control_id: number; + }; + +/** @internal */ +export const AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest$outboundSchema: + z.ZodMiniType< + AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest$Outbound, + AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest + > = z.pipe( + z.object({ + agentName: z.string(), + controlId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + controlId: "control_id", + }); + }), + ); + +export function addAgentControlApiV1AgentsAgentNameControlsControlIdPostRequestToJSON( + addAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest: + AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest, +): string { + return JSON.stringify( + AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest$outboundSchema + .parse(addAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-name-policies-policy-id-post.ts b/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-name-policies-policy-id-post.ts new file mode 100644 index 00000000..919d1948 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/add-agent-policy-api-v1-agents-agent-name-policies-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 AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest = { + agentName: string; + policyId: number; +}; + +/** @internal */ +export type AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest$Outbound = + { + agent_name: string; + policy_id: number; + }; + +/** @internal */ +export const AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest$outboundSchema: + z.ZodMiniType< + AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest$Outbound, + AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest + > = z.pipe( + z.object({ + agentName: z.string(), + policyId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + policyId: "policy_id", + }); + }), + ); + +export function addAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequestToJSON( + addAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest: + AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest, +): string { + return JSON.stringify( + AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest$outboundSchema + .parse(addAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts index 6d3935ba..b016ec01 100644 --- a/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts +++ b/sdks/typescript/src/generated/models/operations/delete-control-api-v1-controls-control-id-delete.ts @@ -8,7 +8,7 @@ import { remap as remap$ } from "../../lib/primitives.js"; export type DeleteControlApiV1ControlsControlIdDeleteRequest = { controlId: number; /** - * If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. + * If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ force?: boolean | undefined; }; diff --git a/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-name-policies-get.ts b/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-name-policies-get.ts new file mode 100644 index 00000000..37010486 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/get-agent-policies-api-v1-agents-agent-name-policies-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 GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest = { + agentName: string; +}; + +/** @internal */ +export type GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest$Outbound = { + agent_name: string; +}; + +/** @internal */ +export const GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest$outboundSchema: + z.ZodMiniType< + GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest$Outbound, + GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function getAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequestToJSON( + getAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest: + GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest, +): string { + return JSON.stringify( + GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest$outboundSchema.parse( + getAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/index.ts b/sdks/typescript/src/generated/models/operations/index.ts index 8e6bebc2..584ee469 100644 --- a/sdks/typescript/src/generated/models/operations/index.ts +++ b/sdks/typescript/src/generated/models/operations/index.ts @@ -2,6 +2,8 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +export * from "./add-agent-control-api-v1-agents-agent-name-controls-control-id-post.js"; +export * from "./add-agent-policy-api-v1-agents-agent-name-policies-policy-id-post.js"; 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-name-policy-delete.js"; export * from "./delete-control-api-v1-controls-control-id-delete.js"; @@ -9,6 +11,7 @@ export * from "./delete-evaluator-config-api-v1-evaluator-configs-config-id-dele export * from "./evaluate-api-v1-evaluation-post.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-policies-api-v1-agents-agent-name-policies-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"; @@ -23,6 +26,9 @@ 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-name-patch.js"; export * from "./patch-control-api-v1-controls-control-id-patch.js"; +export * from "./remove-agent-control-api-v1-agents-agent-name-controls-control-id-delete.js"; +export * from "./remove-agent-policy-api-v1-agents-agent-name-policies-policy-id-delete.js"; +export * from "./remove-all-agent-policies-api-v1-agents-agent-name-policies-delete.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-name-policy-policy-id-post.js"; export * from "./set-control-data-api-v1-controls-control-id-data-put.js"; diff --git a/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-name-controls-control-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-name-controls-control-id-delete.ts new file mode 100644 index 00000000..d230f267 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-agent-control-api-v1-agents-agent-name-controls-control-id-delete.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 RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest = + { + agentName: string; + controlId: number; + }; + +/** @internal */ +export type RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest$Outbound = + { + agent_name: string; + control_id: number; + }; + +/** @internal */ +export const RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest$Outbound, + RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest + > = z.pipe( + z.object({ + agentName: z.string(), + controlId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + controlId: "control_id", + }); + }), + ); + +export function removeAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequestToJSON( + removeAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest: + RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest, +): string { + return JSON.stringify( + RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest$outboundSchema + .parse( + removeAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-name-policies-policy-id-delete.ts b/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-name-policies-policy-id-delete.ts new file mode 100644 index 00000000..5cbabf9d --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-agent-policy-api-v1-agents-agent-name-policies-policy-id-delete.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 RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest = + { + agentName: string; + policyId: number; + }; + +/** @internal */ +export type RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest$Outbound = + { + agent_name: string; + policy_id: number; + }; + +/** @internal */ +export const RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest$Outbound, + RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest + > = z.pipe( + z.object({ + agentName: z.string(), + policyId: z.int(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + policyId: "policy_id", + }); + }), + ); + +export function removeAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequestToJSON( + removeAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest: + RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest, +): string { + return JSON.stringify( + RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest$outboundSchema + .parse( + removeAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest, + ), + ); +} diff --git a/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-name-policies-delete.ts b/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-name-policies-delete.ts new file mode 100644 index 00000000..0e909a21 --- /dev/null +++ b/sdks/typescript/src/generated/models/operations/remove-all-agent-policies-api-v1-agents-agent-name-policies-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 RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest = { + agentName: string; +}; + +/** @internal */ +export type RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest$Outbound = + { + agent_name: string; + }; + +/** @internal */ +export const RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest$outboundSchema: + z.ZodMiniType< + RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest$Outbound, + RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest + > = z.pipe( + z.object({ + agentName: z.string(), + }), + z.transform((v) => { + return remap$(v, { + agentName: "agent_name", + }); + }), + ); + +export function removeAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequestToJSON( + removeAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest: + RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest, +): string { + return JSON.stringify( + RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest$outboundSchema + .parse(removeAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest), + ); +} diff --git a/sdks/typescript/src/generated/models/remove-agent-control-response.ts b/sdks/typescript/src/generated/models/remove-agent-control-response.ts new file mode 100644 index 00000000..e0c61fde --- /dev/null +++ b/sdks/typescript/src/generated/models/remove-agent-control-response.ts @@ -0,0 +1,56 @@ +/* + * 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 { safeParse } from "../lib/schemas.js"; +import { Result as SafeParseResult } from "../types/fp.js"; +import * as types from "../types/primitives.js"; +import { SDKValidationError } from "./errors/sdk-validation-error.js"; + +/** + * Response for removing a direct agent-control association. + */ +export type RemoveAgentControlResponse = { + /** + * True if the control remains active via policy association(s) + */ + controlStillActive: boolean; + /** + * True if a direct agent-control link was removed + */ + removedDirectAssociation: boolean; + /** + * Whether the request succeeded + */ + success: boolean; +}; + +/** @internal */ +export const RemoveAgentControlResponse$inboundSchema: z.ZodMiniType< + RemoveAgentControlResponse, + unknown +> = z.pipe( + z.object({ + control_still_active: types.boolean(), + removed_direct_association: types.boolean(), + success: types.boolean(), + }), + z.transform((v) => { + return remap$(v, { + "control_still_active": "controlStillActive", + "removed_direct_association": "removedDirectAssociation", + }); + }), +); + +export function removeAgentControlResponseFromJSON( + jsonString: string, +): SafeParseResult { + return safeParse( + jsonString, + (x) => RemoveAgentControlResponse$inboundSchema.parse(JSON.parse(x)), + `Failed to parse 'RemoveAgentControlResponse' from JSON`, + ); +} diff --git a/sdks/typescript/src/generated/models/set-policy-response.ts b/sdks/typescript/src/generated/models/set-policy-response.ts index 7fa3bfe6..f600a2a0 100644 --- a/sdks/typescript/src/generated/models/set-policy-response.ts +++ b/sdks/typescript/src/generated/models/set-policy-response.ts @@ -9,13 +9,16 @@ import { Result as SafeParseResult } from "../types/fp.js"; import * as types from "../types/primitives.js"; import { SDKValidationError } from "./errors/sdk-validation-error.js"; +/** + * Compatibility response for singular policy assignment endpoint. + */ export type SetPolicyResponse = { /** - * Previous policy id if one was replaced + * Previously associated policy ID, if any */ oldPolicyId?: number | null | undefined; /** - * Whether the policy was successfully assigned + * Whether the request succeeded */ success: boolean; }; diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index 98c0c874..6aee0bf0 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -2,14 +2,20 @@ * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ +import { agentsAddControl } from "../funcs/agents-add-control.js"; +import { agentsAddPolicy } from "../funcs/agents-add-policy.js"; import { agentsDeletePolicy } from "../funcs/agents-delete-policy.js"; import { agentsGetEvaluator } from "../funcs/agents-get-evaluator.js"; +import { agentsGetPolicies } from "../funcs/agents-get-policies.js"; import { agentsGetPolicy } from "../funcs/agents-get-policy.js"; import { agentsGet } from "../funcs/agents-get.js"; import { agentsInit } from "../funcs/agents-init.js"; import { agentsListControls } from "../funcs/agents-list-controls.js"; import { agentsListEvaluators } from "../funcs/agents-list-evaluators.js"; import { agentsList } from "../funcs/agents-list.js"; +import { agentsRemoveAllAgentPolicies } from "../funcs/agents-remove-all-agent-policies.js"; +import { agentsRemoveControl } from "../funcs/agents-remove-control.js"; +import { agentsRemovePolicy } from "../funcs/agents-remove-policy.js"; import { agentsUpdatePolicy } from "../funcs/agents-update-policy.js"; import { agentsUpdate } from "../funcs/agents-update.js"; import { ClientSDK, RequestOptions } from "../lib/sdks.js"; @@ -24,7 +30,7 @@ export class Agents extends ClientSDK { * @remarks * List all registered agents with cursor-based pagination. * - * Returns a summary of each agent including identifier, policy assignment, + * Returns a summary of each agent including identifier, policy associations, * and counts of registered steps and evaluators. * * Args: @@ -147,15 +153,14 @@ export class Agents extends ClientSDK { * @remarks * List all protection controls active for an agent. * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. + * Controls include the union of policy-derived and directly associated controls. * * Args: * agent_name: Agent identifier * db: Database session (injected) * * Returns: - * AgentControlsResponse with list of controls (empty if no policy) + * AgentControlsResponse with list of active controls * * Raises: * HTTPException 404: Agent not found @@ -171,6 +176,42 @@ export class Agents extends ClientSDK { )); } + /** + * Remove direct control association from agent + * + * @remarks + * Remove a direct control association from an agent (idempotent). + */ + async removeControl( + request: + operations.RemoveAgentControlApiV1AgentsAgentNameControlsControlIdDeleteRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsRemoveControl( + this, + request, + options, + )); + } + + /** + * Associate control directly with agent + * + * @remarks + * Associate a control directly with an agent (idempotent). + */ + async addControl( + request: + operations.AddAgentControlApiV1AgentsAgentNameControlsControlIdPostRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsAddControl( + this, + request, + options, + )); + } + /** * List agent's registered evaluator schemas * @@ -235,23 +276,84 @@ export class Agents extends ClientSDK { } /** - * Remove agent's policy assignment + * Remove all policy associations from agent * * @remarks - * Remove the policy assignment from an agent. + * Remove all policy associations from an agent. + */ + async removeAllAgentPolicies( + request: + operations.RemoveAllAgentPoliciesApiV1AgentsAgentNamePoliciesDeleteRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsRemoveAllAgentPolicies( + this, + request, + options, + )); + } + + /** + * List policies associated with agent * - * The agent will no longer have any protection controls active. + * @remarks + * List policy IDs associated with an agent. + */ + async getPolicies( + request: operations.GetAgentPoliciesApiV1AgentsAgentNamePoliciesGetRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsGetPolicies( + this, + request, + options, + )); + } + + /** + * Remove policy association from agent * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) + * @remarks + * Remove a policy association from an agent. * - * Returns: - * DeletePolicyResponse with success flag + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. + */ + async removePolicy( + request: + operations.RemoveAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdDeleteRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsRemovePolicy( + this, + request, + options, + )); + } + + /** + * Associate policy with agent * - * Raises: - * HTTPException 404: Agent not found or agent has no policy assigned - * HTTPException 500: Database error during removal + * @remarks + * Associate a policy with an agent (idempotent). + */ + async addPolicy( + request: + operations.AddAgentPolicyApiV1AgentsAgentNamePoliciesPolicyIdPostRequest, + options?: RequestOptions, + ): Promise { + return unwrapAsync(agentsAddPolicy( + this, + request, + options, + )); + } + + /** + * Remove agent's policy assignment (compatibility) + * + * @remarks + * Compatibility endpoint that removes all policy associations. */ async deletePolicy( request: @@ -266,20 +368,10 @@ export class Agents extends ClientSDK { } /** - * Get agent's assigned policy + * Get agent's assigned policy (compatibility) * * @remarks - * Retrieve the policy currently assigned to an agent. - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * GetPolicyResponse with policy ID - * - * Raises: - * HTTPException 404: Agent not found or agent has no policy assigned + * Compatibility endpoint that returns the first associated policy. */ async getPolicy( request: operations.GetAgentPolicyApiV1AgentsAgentNamePolicyGetRequest, @@ -293,24 +385,10 @@ export class Agents extends ClientSDK { } /** - * Assign policy to agent + * Assign policy to agent (compatibility) * * @remarks - * Assign a policy to an agent, replacing any existing policy assignment. - * - * The agent will immediately inherit all controls from the assigned policy. - * - * Args: - * agent_name: Agent identifier - * 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 + * Compatibility endpoint that replaces all policy associations with one policy. */ async updatePolicy( request: diff --git a/sdks/typescript/src/generated/sdk/controls.ts b/sdks/typescript/src/generated/sdk/controls.ts index e4871a8a..c32be744 100644 --- a/sdks/typescript/src/generated/sdk/controls.ts +++ b/sdks/typescript/src/generated/sdk/controls.ts @@ -113,7 +113,7 @@ export class Controls extends ClientSDK { * @remarks * Delete a control by ID. * - * By default, deletion fails if the control is associated with any policy. + * By default, deletion fails if the control is associated with any policy or agent. * Use force=true to automatically dissociate and delete. * * Args: @@ -122,7 +122,7 @@ export class Controls extends ClientSDK { * db: Database session (injected) * * Returns: - * DeleteControlResponse with success flag and list of dissociated policies + * DeleteControlResponse with success flag and dissociation details * * Raises: * HTTPException 404: Control not found diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index fe22c629..337fd05c 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -65,21 +65,42 @@ export const api = { apiClient.GET('/api/v1/agents/{agent_name}/controls', { params: { path: { agent_name: agentName } }, }), - setPolicy: ( + getPolicies: (agentName: GetAgentPathParams['agent_name']) => + apiClient.GET('/api/v1/agents/{agent_name}/policies', { + params: { path: { agent_name: agentName } }, + }), + addPolicy: ( agentName: GetAgentPathParams['agent_name'], policyId: number ) => - apiClient.POST('/api/v1/agents/{agent_name}/policy/{policy_id}', { + apiClient.POST('/api/v1/agents/{agent_name}/policies/{policy_id}', { params: { path: { agent_name: agentName, policy_id: policyId } }, }), - getPolicy: (agentName: GetAgentPathParams['agent_name']) => - apiClient.GET('/api/v1/agents/{agent_name}/policy', { - params: { path: { agent_name: agentName } }, + removePolicy: ( + agentName: GetAgentPathParams['agent_name'], + policyId: number + ) => + apiClient.DELETE('/api/v1/agents/{agent_name}/policies/{policy_id}', { + params: { path: { agent_name: agentName, policy_id: policyId } }, }), - deletePolicy: (agentName: GetAgentPathParams['agent_name']) => - apiClient.DELETE('/api/v1/agents/{agent_name}/policy', { + clearPolicies: (agentName: GetAgentPathParams['agent_name']) => + apiClient.DELETE('/api/v1/agents/{agent_name}/policies', { params: { path: { agent_name: agentName } }, }), + addControl: ( + agentName: GetAgentPathParams['agent_name'], + controlId: number + ) => + apiClient.POST('/api/v1/agents/{agent_name}/controls/{control_id}', { + params: { path: { agent_name: agentName, control_id: controlId } }, + }), + removeControl: ( + agentName: GetAgentPathParams['agent_name'], + controlId: number + ) => + apiClient.DELETE('/api/v1/agents/{agent_name}/controls/{control_id}', { + params: { path: { agent_name: agentName, control_id: controlId } }, + }), }, evaluators: { list: () => apiClient.GET('/api/v1/evaluators'), diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index 339c1417..a624d2e6 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -4,4101 +4,4440 @@ */ export interface paths { - '/api/v1/agents': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/api/v1/agents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all agents + * @description List all registered agents with cursor-based pagination. + * + * Returns a summary of each agent including identifier, policy associations, + * and counts of registered steps and evaluators. + * + * Args: + * 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) + * + * Returns: + * ListAgentsResponse with agent summaries and pagination info + */ + get: operations["list_agents_api_v1_agents_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/initAgent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Initialize or update an agent + * @description Register a new agent or update an existing agent's steps and metadata. + * + * This endpoint is idempotent: + * - If the agent name doesn't exist, creates a new agent + * - If the agent name exists, updates registration data in place + * + * 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 + * db: Database session (injected) + * + * Returns: + * InitAgentResponse with created flag and active controls (if policy assigned) + */ + post: operations["init_agent_api_v1_agents_initAgent_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent details + * @description Retrieve agent metadata and all registered steps. + * + * Returns the latest version of each step (deduplicated by type+name). + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * GetAgentResponse with agent metadata and step list + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 422: Agent data is corrupted + */ + get: operations["get_agent_api_v1_agents__agent_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify agent (remove steps/evaluators) + * @description Remove steps and/or evaluators from an agent. + * + * This is the complement to initAgent which only adds items. + * Removals are idempotent - attempting to remove non-existent items is not an error. + * + * Args: + * agent_name: Agent identifier + * request: Lists of step/evaluator identifiers to remove + * db: Database session (injected) + * + * Returns: + * PatchAgentResponse with lists of actually removed items + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 500: Database error during update + */ + patch: operations["patch_agent_api_v1_agents__agent_name__patch"]; + trace?: never; + }; + "/api/v1/agents/{agent_name}/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's active controls + * @description List all protection controls active for an agent. + * + * Controls include the union of policy-derived and directly associated controls. + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * AgentControlsResponse with list of active controls + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_controls_api_v1_agents__agent_name__controls_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate control directly with agent + * @description Associate a control directly with an agent (idempotent). + */ + post: operations["add_agent_control_api_v1_agents__agent_name__controls__control_id__post"]; + /** + * Remove direct control association from agent + * @description Remove a direct control association from an agent (idempotent). + */ + delete: operations["remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's registered evaluator schemas + * @description List all evaluator schemas registered with an agent. + * + * Evaluator schemas are registered via initAgent and used for: + * - Config validation when creating Controls + * - UI to display available config options + * + * Args: + * 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) + * + * Returns: + * ListEvaluatorsResponse with evaluator schemas and pagination + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations["list_agent_evaluators_api_v1_agents__agent_name__evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/evaluators/{evaluator_name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get specific evaluator schema + * @description Get a specific evaluator schema registered with an agent. + * + * Args: + * agent_name: Agent identifier + * evaluator_name: Name of the evaluator + * db: Database session (injected) + * + * Returns: + * EvaluatorSchemaItem with schema details + * + * Raises: + * HTTPException 404: Agent or evaluator not found + */ + get: operations["get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policies associated with agent + * @description List policy IDs associated with an agent. + */ + get: operations["get_agent_policies_api_v1_agents__agent_name__policies_get"]; + put?: never; + post?: never; + /** + * Remove all policy associations from agent + * @description Remove all policy associations from an agent. + */ + delete: operations["remove_all_agent_policies_api_v1_agents__agent_name__policies_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policies/{policy_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate policy with agent + * @description Associate a policy with an agent (idempotent). + */ + post: operations["add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post"]; + /** + * Remove policy association from agent + * @description Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. + */ + delete: operations["remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent's assigned policy (compatibility) + * @description Compatibility endpoint that returns the first associated policy. + */ + get: operations["get_agent_policy_api_v1_agents__agent_name__policy_get"]; + put?: never; + post?: never; + /** + * Remove agent's policy assignment (compatibility) + * @description Compatibility endpoint that removes all policy associations. + */ + delete: operations["delete_agent_policy_api_v1_agents__agent_name__policy_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/agents/{agent_name}/policy/{policy_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Assign policy to agent (compatibility) + * @description Compatibility endpoint that replaces all policy associations with one policy. + */ + post: operations["set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/controls": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all controls + * @description List all controls with optional filtering and cursor-based pagination. + * + * Controls are returned ordered by ID descending (newest first). + * + * Args: + * cursor: ID of the last control from the previous page (for pagination) + * limit: Maximum number of controls to return (default 20, max 100) + * name: Optional filter by name (partial, case-insensitive match) + * enabled: Optional filter by enabled status + * step_type: Optional filter by step type (built-ins: 'tool', 'llm') + * stage: Optional filter by stage ('pre' or 'post') + * execution: Optional filter by execution ('server' or 'sdk') + * tag: Optional filter by tag + * db: Database session (injected) + * + * Returns: + * ListControlsResponse with control summaries and pagination info + * + * Example: + * GET /controls?limit=10&enabled=true&step_type=tool + */ + get: operations["list_controls_api_v1_controls_get"]; + /** + * Create a new control + * @description Create a new control with a unique name and empty data. + * + * Controls define protection logic and can be added to policies. + * Use the PUT /{control_id}/data endpoint to set control configuration. + * + * Args: + * request: Control creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreateControlResponse with the new control's ID + * + * Raises: + * HTTPException 409: Control with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations["create_control_api_v1_controls_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control details + * @description Retrieve a control by ID including its name and configuration data. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlResponse with control id, name, and data + * + * Raises: + * HTTPException 404: Control not found + */ + get: operations["get_control_api_v1_controls__control_id__get"]; + put?: never; + post?: never; + /** + * Delete a control + * @description Delete a control by ID. + * + * By default, deletion fails if the control is associated with any policy or agent. + * Use force=true to automatically dissociate and delete. + * + * Args: + * control_id: ID of the control to delete + * force: If true, remove associations before deleting + * db: Database session (injected) + * + * Returns: + * DeleteControlResponse with success flag and dissociation details + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: Control is in use (and force=false) + * HTTPException 500: Database error during deletion + */ + delete: operations["delete_control_api_v1_controls__control_id__delete"]; + options?: never; + head?: never; + /** + * Update control metadata + * @description Update control metadata (name and/or enabled status). + * + * This endpoint allows partial updates: + * - To rename: provide 'name' field + * - To enable/disable: provide 'enabled' field (updates the control's data) + * + * Args: + * control_id: ID of the control to update + * request: Fields to update (name, enabled) + * db: Database session (injected) + * + * Returns: + * PatchControlResponse with current control state + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: New name conflicts with existing control + * HTTPException 422: Cannot update enabled status (control has no data configured) + * HTTPException 500: Database error during update + */ + patch: operations["patch_control_api_v1_controls__control_id__patch"]; + trace?: never; + }; + "/api/v1/controls/{control_id}/data": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control configuration data + * @description Retrieve the configuration data for a control. + * + * Control data is a JSONB field that must follow the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlDataResponse with validated ControlDefinition + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 422: Control data is corrupted + */ + get: operations["get_control_data_api_v1_controls__control_id__data_get"]; + /** + * Update control configuration data + * @description Update the configuration data for a control. + * + * This replaces the entire data payload. The data is validated against + * the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * request: New control data (replaces existing) + * db: Database session (injected) + * + * Returns: + * SetControlDataResponse with success flag + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 500: Database error during update + */ + put: operations["set_control_data_api_v1_controls__control_id__data_put"]; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** List evaluator configs */ + get: operations["list_evaluator_configs_api_v1_evaluator_configs_get"]; + put?: never; + /** Create evaluator config */ + post: operations["create_evaluator_config_api_v1_evaluator_configs_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluator-configs/{config_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get evaluator config */ + get: operations["get_evaluator_config_api_v1_evaluator_configs__config_id__get"]; + /** Update evaluator config */ + put: operations["update_evaluator_config_api_v1_evaluator_configs__config_id__put"]; + post?: never; + /** Delete evaluator config */ + delete: operations["delete_evaluator_config_api_v1_evaluator_configs__config_id__delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/evaluators": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available evaluators + * @description List all available evaluators. + * + * Returns metadata and JSON Schema for each built-in evaluator. + * + * Built-in evaluators: + * - **regex**: Regular expression pattern matching + * - **list**: List-based value matching with flexible logic + * - **json**: JSON validation with schema, types, constraints + * - **sql**: SQL query validation + * + * Custom evaluators are registered per-agent via initAgent. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. + */ + get: operations["get_evaluators_api_v1_evaluators_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ingest Events + * @description Ingest batched control execution events. + * + * Events are stored directly to the database with ~5-20ms latency. + * + * Args: + * request: Batch of events to ingest + * ingestor: Event ingestor (injected) + * + * Returns: + * BatchEventsResponse with counts of received/processed/dropped + */ + post: operations["ingest_events_api_v1_observability_events_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/events/query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Query Events + * @description Query raw control execution events. + * + * Supports filtering by: + * - 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_name: Filter by agent + * - control_ids: Filter by controls + * - actions: Filter by actions (allow, deny, warn, log) + * - matched: Filter by matched status + * - check_stages: Filter by check stage (pre, post) + * - applies_to: Filter by call type (llm_call, tool_call) + * - start_time/end_time: Filter by time range + * + * Results are paginated with limit/offset. + * + * Args: + * request: Query parameters + * store: Event store (injected) + * + * Returns: + * EventQueryResponse with matching events and pagination info + */ + post: operations["query_events_api_v1_observability_events_query_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Stats + * @description Get agent-level aggregated statistics. + * + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. + * + * Args: + * 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) + * + * Returns: + * StatsResponse with agent-level totals and per-control breakdown + */ + get: operations["get_stats_api_v1_observability_stats_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/stats/controls/{control_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID 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) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/observability/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Status + * @description Get observability system status. + * + * Returns basic health information. + */ + get: operations["get_status_api_v1_observability_status_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/policies": { + 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. + * + * 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; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * List all agents - * @description List all registered agents with cursor-based pagination. - * - * Returns a summary of each agent including identifier, policy assignment, - * and counts of registered steps and evaluators. - * - * Args: - * 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) - * - * Returns: - * ListAgentsResponse with agent summaries and pagination info - */ - get: operations['list_agents_api_v1_agents_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/initAgent': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Initialize or update an agent - * @description Register a new agent or update an existing agent's steps and metadata. - * - * This endpoint is idempotent: - * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists, updates registration data in place - * - * 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 - * db: Database session (injected) - * - * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) - */ - post: operations['init_agent_api_v1_agents_initAgent_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent details - * @description Retrieve agent metadata and all registered steps. - * - * Returns the latest version of each step (deduplicated by type+name). - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * GetAgentResponse with agent metadata and step list - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 422: Agent data is corrupted - */ - get: operations['get_agent_api_v1_agents__agent_name__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify agent (remove steps/evaluators) - * @description Remove steps and/or evaluators from an agent. - * - * This is the complement to initAgent which only adds items. - * Removals are idempotent - attempting to remove non-existent items is not an error. - * - * Args: - * agent_name: Agent identifier - * request: Lists of step/evaluator identifiers to remove - * db: Database session (injected) - * - * Returns: - * PatchAgentResponse with lists of actually removed items - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 500: Database error during update - */ - patch: operations['patch_agent_api_v1_agents__agent_name__patch']; - trace?: never; - }; - '/api/v1/agents/{agent_name}/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's active controls - * @description List all protection controls active for an agent. - * - * Controls are inherited from the agent's assigned policy. - * Returns an empty list if the agent has no policy. - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * AgentControlsResponse with list of controls (empty if no policy) - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_controls_api_v1_agents__agent_name__controls_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/evaluators': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's registered evaluator schemas - * @description List all evaluator schemas registered with an agent. - * - * Evaluator schemas are registered via initAgent and used for: - * - Config validation when creating Controls - * - UI to display available config options - * - * Args: - * 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) - * - * Returns: - * ListEvaluatorsResponse with evaluator schemas and pagination - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations['list_agent_evaluators_api_v1_agents__agent_name__evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/evaluators/{evaluator_name}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get specific evaluator schema - * @description Get a specific evaluator schema registered with an agent. - * - * Args: - * agent_name: Agent identifier - * evaluator_name: Name of the evaluator - * db: Database session (injected) - * - * Returns: - * EvaluatorSchemaItem with schema details - * - * Raises: - * HTTPException 404: Agent or evaluator not found - */ - get: operations['get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/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_name: Agent identifier - * 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_name__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_name: Agent identifier - * 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_name__policy_delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/agents/{agent_name}/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_name: Agent identifier - * 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_name__policy__policy_id__post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/controls': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all controls - * @description List all controls with optional filtering and cursor-based pagination. - * - * Controls are returned ordered by ID descending (newest first). - * - * Args: - * cursor: ID of the last control from the previous page (for pagination) - * limit: Maximum number of controls to return (default 20, max 100) - * name: Optional filter by name (partial, case-insensitive match) - * enabled: Optional filter by enabled status - * step_type: Optional filter by step type (built-ins: 'tool', 'llm') - * stage: Optional filter by stage ('pre' or 'post') - * execution: Optional filter by execution ('server' or 'sdk') - * tag: Optional filter by tag - * db: Database session (injected) - * - * Returns: - * ListControlsResponse with control summaries and pagination info - * - * Example: - * GET /controls?limit=10&enabled=true&step_type=tool - */ - get: operations['list_controls_api_v1_controls_get']; - /** - * Create a new control - * @description Create a new control with a unique name and empty data. - * - * Controls define protection logic and can be added to policies. - * Use the PUT /{control_id}/data endpoint to set control configuration. - * - * Args: - * request: Control creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreateControlResponse with the new control's ID - * - * Raises: - * HTTPException 409: Control with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations['create_control_api_v1_controls_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control details - * @description Retrieve a control by ID including its name and configuration data. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlResponse with control id, name, and data - * - * Raises: - * HTTPException 404: Control not found - */ - get: operations['get_control_api_v1_controls__control_id__get']; - put?: never; - post?: never; - /** - * Delete a control - * @description Delete a control by ID. - * - * By default, deletion fails if the control is associated with any policy. - * Use force=true to automatically dissociate and delete. - * - * Args: - * control_id: ID of the control to delete - * force: If true, remove associations before deleting - * db: Database session (injected) - * - * Returns: - * DeleteControlResponse with success flag and list of dissociated policies - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: Control is in use (and force=false) - * HTTPException 500: Database error during deletion - */ - delete: operations['delete_control_api_v1_controls__control_id__delete']; - options?: never; - head?: never; - /** - * Update control metadata - * @description Update control metadata (name and/or enabled status). - * - * This endpoint allows partial updates: - * - To rename: provide 'name' field - * - To enable/disable: provide 'enabled' field (updates the control's data) - * - * Args: - * control_id: ID of the control to update - * request: Fields to update (name, enabled) - * db: Database session (injected) - * - * Returns: - * PatchControlResponse with current control state - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: New name conflicts with existing control - * HTTPException 422: Cannot update enabled status (control has no data configured) - * HTTPException 500: Database error during update - */ - patch: operations['patch_control_api_v1_controls__control_id__patch']; - trace?: never; - }; - '/api/v1/controls/{control_id}/data': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control configuration data - * @description Retrieve the configuration data for a control. - * - * Control data is a JSONB field that must follow the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlDataResponse with validated ControlDefinition - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 422: Control data is corrupted - */ - get: operations['get_control_data_api_v1_controls__control_id__data_get']; - /** - * Update control configuration data - * @description Update the configuration data for a control. - * - * This replaces the entire data payload. The data is validated against - * the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * request: New control data (replaces existing) - * db: Database session (injected) - * - * Returns: - * SetControlDataResponse with success flag - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 500: Database error during update - */ - put: operations['set_control_data_api_v1_controls__control_id__data_put']; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** List evaluator configs */ - get: operations['list_evaluator_configs_api_v1_evaluator_configs_get']; - put?: never; - /** Create evaluator config */ - post: operations['create_evaluator_config_api_v1_evaluator_configs_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluator-configs/{config_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get evaluator config */ - get: operations['get_evaluator_config_api_v1_evaluator_configs__config_id__get']; - /** Update evaluator config */ - put: operations['update_evaluator_config_api_v1_evaluator_configs__config_id__put']; - post?: never; - /** Delete evaluator config */ - delete: operations['delete_evaluator_config_api_v1_evaluator_configs__config_id__delete']; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/evaluators': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List available evaluators - * @description List all available evaluators. - * - * Returns metadata and JSON Schema for each built-in evaluator. - * - * Built-in evaluators: - * - **regex**: Regular expression pattern matching - * - **list**: List-based value matching with flexible logic - * - **json**: JSON validation with schema, types, constraints - * - **sql**: SQL query validation - * - * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. - */ - get: operations['get_evaluators_api_v1_evaluators_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ingest Events - * @description Ingest batched control execution events. - * - * Events are stored directly to the database with ~5-20ms latency. - * - * Args: - * request: Batch of events to ingest - * ingestor: Event ingestor (injected) - * - * Returns: - * BatchEventsResponse with counts of received/processed/dropped - */ - post: operations['ingest_events_api_v1_observability_events_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/events/query': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Query Events - * @description Query raw control execution events. - * - * Supports filtering by: - * - 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_name: Filter by agent - * - control_ids: Filter by controls - * - actions: Filter by actions (allow, deny, steer, warn, log) - * - matched: Filter by matched status - * - check_stages: Filter by check stage (pre, post) - * - applies_to: Filter by call type (llm_call, tool_call) - * - start_time/end_time: Filter by time range - * - * Results are paginated with limit/offset. - * - * Args: - * request: Query parameters - * store: Event store (injected) - * - * Returns: - * EventQueryResponse with matching events and pagination info - */ - post: operations['query_events_api_v1_observability_events_query_post']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Stats - * @description Get agent-level aggregated statistics. - * - * Returns totals across all controls plus per-control breakdown. - * Use /stats/controls/{control_id} for single control stats. - * - * Args: - * 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) - * - * Returns: - * StatsResponse with agent-level totals and per-control breakdown - */ - get: operations['get_stats_api_v1_observability_stats_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/stats/controls/{control_id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Control Stats - * @description Get statistics for a single control. - * - * Returns stats for the specified control with optional time-series. - * - * Args: - * control_id: Control ID 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) - * - * Returns: - * ControlStatsResponse with control stats and optional timeseries - */ - get: operations['get_control_stats_api_v1_observability_stats_controls__control_id__get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/observability/status': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Status - * @description Get observability system status. - * - * Returns basic health information. - */ - get: operations['get_status_api_v1_observability_status_get']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/policies': { - 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. - * - * 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; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - /** - * Agent - * @description Agent metadata for registration and tracking. - * - * An agent represents an AI system that can be protected and monitored. - * 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_metadata": { - * "environment": "production", - * "team": "support" - * }, - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * } - */ - Agent: { - /** - * Agent Created At - * @description ISO 8601 timestamp when agent was created - */ - agent_created_at?: string | null; - /** - * Agent Description - * @description Optional description of the agent's purpose - */ - agent_description?: string | null; - /** - * Agent Metadata - * @description Free-form metadata dictionary for custom properties - */ - 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 - */ - agent_updated_at?: string | null; - /** - * Agent Version - * @description Semantic version string (e.g. '1.0.0') - */ - agent_version?: string | null; - }; - /** AgentControlsResponse */ - AgentControlsResponse: { - /** - * Controls - * @description List of controls associated with the agent via its policy - */ - controls: components['schemas']['Control'][]; - }; - /** - * AgentRef - * @description Reference to an agent (for listing which agents use a control). - */ - AgentRef: { - /** - * Agent Name - * @description Agent identifier - */ - agent_name: string; - }; - /** - * AgentSummary - * @description Summary of an agent for list responses. - */ - AgentSummary: { - /** - * Active Controls Count - * @description Number of active controls from agent's policy - * @default 0 - */ - active_controls_count: number; - /** - * Agent Name - * @description Unique identifier of the agent - */ - agent_name: string; - /** - * Created At - * @description ISO 8601 timestamp when agent was created - */ - created_at?: string | null; - /** - * Evaluator Count - * @description Number of evaluators registered with the agent - * @default 0 - */ - evaluator_count: number; - /** - * 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 - */ - step_count: number; - }; - /** AssocResponse */ - AssocResponse: { - /** - * Success - * @description Whether the association change succeeded - */ - success: boolean; - }; - /** - * BatchEventsRequest - * @description Request model for batch event ingestion. - * - * SDKs batch events and send them to the server periodically. - * This reduces HTTP overhead significantly (100x reduction). - * - * Attributes: - * events: List of control execution events to ingest - * @example { - * "events": [ - * { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_id": 123, - * "control_name": "sql-injection-check", - * "matched": true, - * "span_id": "00f067aa0ba902b7", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * ] - * } - */ - BatchEventsRequest: { - /** - * Events - * @description List of events to ingest - */ - events: components['schemas']['ControlExecutionEvent'][]; - }; - /** - * BatchEventsResponse - * @description Response model for batch event ingestion. - * - * Attributes: - * received: Number of events received - * enqueued: Number of events successfully enqueued - * dropped: Number of events dropped (queue full) - * status: Overall status ('queued', 'partial', 'failed') - */ - BatchEventsResponse: { - /** - * Dropped - * @description Number of events dropped - */ - dropped: number; - /** - * Enqueued - * @description Number of events enqueued - */ - enqueued: number; - /** - * Received - * @description Number of events received - */ - received: number; - /** - * Status - * @description Overall ingestion status - * @enum {string} - */ - 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. - * - * Note: Only fully-configured controls (with valid ControlDefinition) - * are returned from API endpoints. Unconfigured controls are filtered out. - */ - Control: { - control: components['schemas']['ControlDefinition']; - /** Id */ - id: number; - /** Name */ - name: string; - }; - /** - * SteeringContext - * @description Steering context for steer actions. - * - * This model provides an extensible structure for steering guidance. - * Future fields could include severity, categories, suggested_actions, etc. - * @example { - * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." - * } - */ - SteeringContext: { - /** - * Message - * @description Guidance message explaining what needs to be corrected and how - */ - message: string; - }; - /** - * ControlAction - * @description What to do when control matches. - */ - ControlAction: { - /** - * Decision - * @description Action to take when control is triggered - * @enum {string} - */ - decision: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; - /** - * Steering Context - * @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. - */ - steering_context?: components['schemas']['SteeringContext'] | null; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - ControlDefinition: { - /** @description What action to take when control matches */ - action: components['schemas']['ControlAction']; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components['schemas']['EvaluatorSpec']; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: 'server' | 'sdk'; - /** @description Which steps and stages this control applies to */ - scope?: components['schemas']['ControlScope']; - /** @description What data to select from the payload */ - selector: components['schemas']['ControlSelector']; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlExecutionEvent - * @description Represents a single control execution event. - * - * This is the core observability data model, capturing: - * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) - * - Context: agent, control, check stage, applies to - * - Result: action taken, whether matched, confidence score - * - Timing: when it happened, how long it took - * - Optional details: evaluator name, selector path, errors, metadata - * - * Attributes: - * 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_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) - * applies_to: "llm_call" or "tool_call" - * action: The action taken (allow, deny, steer, warn, log) - * matched: Whether the control evaluator matched - * confidence: Confidence score from the evaluator (0.0-1.0) - * timestamp: When the control was executed (UTC) - * execution_duration_ms: How long the control evaluation took - * evaluator_name: Name of the evaluator used - * selector_path: The selector path used to extract data - * error_message: Error message if evaluation failed - * metadata: Additional metadata for extensibility - * @example { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", - * "control_id": 123, - * "control_name": "sql-injection-check", - * "evaluator_name": "regex", - * "execution_duration_ms": 15.3, - * "matched": true, - * "selector_path": "input", - * "span_id": "00f067aa0ba902b7", - * "timestamp": "2025-01-09T10:30:00Z", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - */ - ControlExecutionEvent: { - /** - * Action - * @description Action taken by the control - * @enum {string} - */ - action: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; - /** - * Agent Name - * @description Identifier of the agent - */ - agent_name: string; - /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} - */ - applies_to: 'llm_call' | 'tool_call'; - /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} - */ - check_stage: 'pre' | 'post'; - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Control Execution Id - * @description Unique ID for this control execution - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control (denormalized) - */ - control_name: string; - /** - * Error Message - * @description Error message if evaluation failed - */ - error_message?: string | null; - /** - * Evaluator Name - * @description Name of the evaluator used - */ - evaluator_name?: string | null; - /** - * Execution Duration Ms - * @description Execution duration in milliseconds - */ - execution_duration_ms?: number | null; - /** - * Matched - * @description Whether the evaluator matched (True) or not (False) - */ - matched: boolean; - /** - * Metadata - * @description Additional metadata - */ - metadata?: { - [key: string]: unknown; - }; - /** - * Selector Path - * @description Selector path used to extract data - */ - selector_path?: string | null; - /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) - */ - span_id: string; - /** - * Timestamp - * Format: date-time - * @description When the control was executed (UTC) - */ - 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' | 'steer' | 'warn' | 'log'; - /** - * Control Execution Id - * @description Unique ID for this control execution (generated by engine) - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control - */ - control_name: string; - /** @description Evaluator result (confidence, message, metadata) */ - result: components['schemas']['EvaluatorResult']; - }; - /** - * ControlScope - * @description Defines when a control applies to a Step. - * @example { - * "stages": [ - * "pre" - * ], - * "step_types": [ - * "tool" - * ] - * } - * @example { - * "step_names": [ - * "search_db", - * "fetch_user" - * ] - * } - * @example { - * "step_name_regex": "^db_.*" - * } - * @example { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * } - */ - ControlScope: { - /** - * Stages - * @description Evaluation stages this control applies to - */ - stages?: ('pre' | 'post')[] | null; - /** - * Step Name Regex - * @description RE2 pattern matched with search() against step name - */ - step_name_regex?: string | null; - /** - * Step Names - * @description Exact step names this control applies to - */ - 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 - * @description Selects data from a Step payload. - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - * @example { - * "path": "output" - * } - * @example { - * "path": "context.user_id" - * } - * @example { - * "path": "input" - * } - * @example { - * "path": "*" - * } - * @example { - * "path": "name" - * } - * @example { - * "path": "output" - * } - */ - ControlSelector: { - /** - * Path - * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' - * @default * - */ - path: string | null; - }; - /** - * ControlStats - * @description Aggregated statistics for a single control. - * - * Attributes: - * control_id: Database ID of the control - * control_name: Name of the control - * execution_count: Total number of executions - * match_count: Number of times the control matched - * non_match_count: Number of times the control did not match - * allow_count: Number of allow actions - * deny_count: Number of deny actions - * warn_count: Number of warn actions - * log_count: Number of log actions - * error_count: Number of errors during evaluation - * avg_confidence: Average confidence score - * 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 - */ - control_id: number; - /** - * Control Name - * @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 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - */ - non_match_count: number; - /** - * Steer Count - * @description Steer actions - */ - steer_count: number; - /** - * Warn Count - * @description Warn actions - */ - warn_count: number; - }; - /** - * ControlStatsResponse - * @description Response model for control-level statistics. - * - * Contains stats for a single control (with optional timeseries). - * - * Attributes: - * agent_name: Agent identifier - * time_range: Time range used - * control_id: Control ID - * control_name: Control name - * stats: Control statistics (includes timeseries when requested) - */ - ControlStatsResponse: { - /** - * Agent Name - * @description Agent identifier - */ - agent_name: string; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - 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: { - /** - * Description - * @description Control description - */ - description?: string | null; - /** - * Enabled - * @description Whether control is enabled - * @default true - */ - enabled: boolean; - /** - * Execution - * @description 'server' or 'sdk' - */ - execution?: string | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - /** - * Stages - * @description Evaluation stages in scope - */ - stages?: string[] | null; - /** - * Step Types - * @description Step types in scope - */ - step_types?: string[] | null; - /** - * Tags - * @description Control tags - */ - tags?: string[]; - /** @description Agent using this control */ - used_by_agent?: components['schemas']['AgentRef'] | null; - }; - /** CreateControlRequest */ - CreateControlRequest: { - /** - * Name - * @description Unique control name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreateControlResponse */ - CreateControlResponse: { - /** - * Control Id - * @description Identifier of the created control - */ - control_id: number; - }; - /** - * CreateEvaluatorConfigRequest - * @description Request to create an evaluator config template. - */ - CreateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyRequest */ - CreatePolicyRequest: { - /** - * Name - * @description Unique policy name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyResponse */ - CreatePolicyResponse: { - /** - * Policy Id - * @description Identifier of the created policy - */ - policy_id: number; - }; - /** - * DeleteControlResponse - * @description Response for deleting a control. - */ - DeleteControlResponse: { - /** - * 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 - * @description Response for deleting an evaluator config. - */ - DeleteEvaluatorConfigResponse: { - /** - * Success - * @description Whether the evaluator config was deleted - */ - success: boolean; - }; - /** DeletePolicyResponse */ - DeletePolicyResponse: { - /** - * Success - * @description Whether the policy was successfully removed - */ - success: boolean; - }; - /** - * EvaluationRequest - * @description Request model for evaluation analysis. - * - * Used to analyze agent interactions for safety violations, - * policy compliance, and control rules. - * - * Attributes: - * 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_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "output": "I cannot share sensitive payment information.", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "type": "tool" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "output": { - * "results": [] - * }, - * "type": "tool" - * } - * } - */ - EvaluationRequest: { - /** - * Agent Name - * @description Identifier of the agent making the evaluation request - */ - 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 - * @description Response model from evaluation analysis (server-side). - * - * This is what the server returns. The SDK may transform this - * into an EvaluationResult for client convenience. - * - * Attributes: - * is_safe: Whether the content is considered safe - * confidence: Confidence score between 0.0 and 1.0 - * reason: Optional explanation for the decision - * matches: List of controls that matched/triggered (if any) - * errors: List of controls that failed during evaluation (if any) - * non_matches: List of controls that were evaluated but did not match (if any) - */ - EvaluationResponse: { - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Errors - * @description List of controls that failed during evaluation (if any) - */ - 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; - /** - * Non Matches - * @description List of controls that were evaluated but did not match (if any) - */ - non_matches?: components['schemas']['ControlMatch'][] | null; - /** - * Reason - * @description Explanation for the decision - */ - reason?: string | null; - }; - /** - * EvaluatorConfigItem - * @description Evaluator config template stored in the server. - */ - EvaluatorConfigItem: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Created At - * @description ISO 8601 created timestamp - */ - created_at?: string | null; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Id - * @description Evaluator config ID - */ - id: number; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Updated At - * @description ISO 8601 updated timestamp - */ - updated_at?: string | null; - }; - /** - * EvaluatorInfo - * @description Information about a registered evaluator. - */ - EvaluatorInfo: { - /** - * Config Schema - * @description JSON Schema for config - */ - 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 - */ - requires_api_key: boolean; - /** - * Timeout Ms - * @description Default timeout in milliseconds - */ - timeout_ms: number; - /** - * Version - * @description Evaluator version - */ - version: string; - }; - /** - * EvaluatorResult - * @description Result from a control evaluator. - * - * The `error` field indicates evaluator failures, NOT validation failures: - * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors - * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures - * - * When `error` is set, `matched` must be False (fail-open on evaluator errors). - * When `error` is None, `matched` reflects the actual validation result. - * - * This distinction allows: - * - Clients to distinguish "data violated rules" from "evaluator is broken" - * - Observability systems to monitor evaluator health separately from validation outcomes - */ - EvaluatorResult: { - /** - * 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 - */ - message?: string | null; - /** - * Metadata - * @description Additional result metadata - */ - metadata?: { - [key: string]: unknown; - } | null; - }; - /** - * EvaluatorSchema - * @description Schema for a custom evaluator registered with an agent. - * - * Custom evaluators are Evaluator classes deployed with the engine. - * This schema is registered via initAgent for validation and UI purposes. - */ - EvaluatorSchema: { - /** - * Config Schema - * @description JSON Schema for evaluator config validation - */ - config_schema?: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Name - * @description Unique evaluator name - */ - name: string; - }; - /** - * EvaluatorSchemaItem - * @description Evaluator schema summary for list response. - */ - EvaluatorSchemaItem: { - /** 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 - * @description Request model for querying raw events. - * - * Supports filtering by various criteria and pagination. - * - * Attributes: - * 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_name: Filter by agent identifier - * control_ids: Filter by control IDs - * actions: Filter by actions (allow, deny, steer, 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_name": "my-agent", - * "limit": 50, - * "start_time": "2025-01-09T00:00:00Z" - * } - */ - EventQueryRequest: { - /** - * Actions - * @description Filter by actions - */ - actions?: ('allow' | 'deny' | 'steer' | 'warn' | 'log')[] | null; - /** - * Agent Name - * @description Filter by agent identifier - */ - 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; - /** - * Control Execution Id - * @description Filter by specific event ID - */ - control_execution_id?: string | null; - /** - * Control Ids - * @description Filter by control IDs - */ - control_ids?: number[] | null; - /** - * End Time - * @description Filter events before this time - */ - end_time?: string | null; - /** - * Limit - * @description Maximum events - * @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 - * @description Response model for event queries. - * - * Attributes: - * events: List of matching events - * total: Total number of matching events (for pagination) - * limit: Limit used in query - * offset: Offset used in query - */ - EventQueryResponse: { - /** - * Events - * @description Matching events - */ - events: components['schemas']['ControlExecutionEvent'][]; - /** - * Limit - * @description Limit used in query - */ - limit: number; - /** - * Offset - * @description Offset used in query - */ - offset: number; - /** - * Total - * @description Total matching events - */ - total: number; - }; - /** - * GetAgentResponse - * @description Response containing agent details and registered steps. - */ - GetAgentResponse: { - /** @description Agent metadata */ - agent: components['schemas']['Agent']; - /** - * 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: { - /** @description Control data payload */ - data: components['schemas']['ControlDefinition']; - }; - /** - * GetControlResponse - * @description Response containing control details. - */ - GetControlResponse: { - /** @description Control configuration data (None if not yet configured) */ - data?: components['schemas']['ControlDefinition'] | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - }; - /** - * GetPolicyControlsResponse - * @description Response containing control IDs associated with a policy. - */ - GetPolicyControlsResponse: { - /** - * Control Ids - * @description List of control IDs associated with the policy - */ - control_ids: number[]; - }; - /** GetPolicyResponse */ - GetPolicyResponse: { - /** - * Policy Id - * @description Identifier of the policy assigned to the agent - */ - policy_id: number; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components['schemas']['ValidationError'][]; - }; - /** - * HealthResponse - * @description Health check response model. - * - * Attributes: - * status: Current health status (e.g., "healthy", "degraded", "unhealthy") - * version: Application version - */ - HealthResponse: { - /** Status */ - status: string; - /** 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_name": "customer-service-bot", - * "agent_version": "1.0.0" - * }, - * "evaluators": [ - * { - * "config_schema": { - * "properties": { - * "sensitivity": { - * "type": "string" - * } - * }, - * "type": "object" - * }, - * "description": "Detects PII in text", - * "name": "pii-detector" - * } - * ], - * "steps": [ - * { - * "input_schema": { - * "query": { - * "type": "string" - * } - * }, - * "name": "search_kb", - * "output_schema": { - * "results": { - * "type": "array" - * } - * }, - * "type": "tool" - * } - * ] - * } - */ - InitAgentRequest: { - /** @description Agent metadata including ID, name, and version */ - agent: components['schemas']['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 - */ - conflict_mode: components['schemas']['ConflictMode']; - /** - * Evaluators - * @description Custom evaluator schemas for config validation - */ - evaluators?: components['schemas']['EvaluatorSchema'][]; - /** - * Force Replace - * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. - * @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; - /** - * Overwrite Applied - * @description True if overwrite mode changed registration data on an existing agent - * @default false - */ - overwrite_applied: boolean; - /** @description Detailed list of changes applied in overwrite mode */ - overwrite_changes?: components['schemas']['InitAgentOverwriteChanges']; - }; - JSONObject: { - [key: string]: components['schemas']['JSONValue']; - }; - /** @description Any JSON value */ - JSONValue: unknown; - /** - * ListAgentsResponse - * @description Response for listing agents. - */ - ListAgentsResponse: { - /** - * Agents - * @description List of agent summaries - */ - agents: components['schemas']['AgentSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListControlsResponse - * @description Response for listing controls. - */ - ListControlsResponse: { - /** - * Controls - * @description List of control summaries - */ - controls: components['schemas']['ControlSummary'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorConfigsResponse - * @description Response for listing evaluator configs. - */ - ListEvaluatorConfigsResponse: { - /** - * Evaluator Configs - * @description List of evaluator configs - */ - evaluator_configs: components['schemas']['EvaluatorConfigItem'][]; - /** @description Pagination metadata */ - pagination: components['schemas']['PaginationInfo']; - }; - /** - * ListEvaluatorsResponse - * @description Response for listing agent's evaluator schemas. - */ - ListEvaluatorsResponse: { - /** Evaluators */ - evaluators: components['schemas']['EvaluatorSchemaItem'][]; - pagination: components['schemas']['PaginationInfo']; - }; - /** - * PaginationInfo - * @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; - /** - * Next Cursor - * @description Cursor for fetching the next page (null if no more pages) - */ - next_cursor?: string | null; - /** - * Total - * @description Total number of items - */ - total: number; - }; - /** - * PatchAgentRequest - * @description Request to modify an agent (remove steps/evaluators). - */ - PatchAgentRequest: { - /** - * 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: { - /** - * 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: { - /** - * Enabled - * @description Enable or disable the control - */ - enabled?: boolean | null; - /** - * Name - * @description New name for the control - */ - name?: string | null; - }; - /** - * PatchControlResponse - * @description Response from control metadata update. - */ - PatchControlResponse: { - /** - * Enabled - * @description Current enabled status (if control has data configured) - */ - enabled?: boolean | null; - /** - * Name - * @description Current control name (may have changed) - */ - name: string; - /** - * Success - * @description Whether the update succeeded - */ - success: boolean; - }; - /** - * SetControlDataRequest - * @description Request to update control configuration data. - */ - SetControlDataRequest: { - /** @description Control configuration data (replaces existing) */ - data: components['schemas']['ControlDefinition']; - }; - /** SetControlDataResponse */ - SetControlDataResponse: { - /** - * Success - * @description Whether the control data was updated - */ - success: boolean; - }; - /** SetPolicyResponse */ - SetPolicyResponse: { - /** - * 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 - * @description Response model for agent-level aggregated statistics. - * - * Contains agent-level totals (with optional timeseries) and per-control breakdown. - * - * Attributes: - * 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 Name - * @description Agent identifier - */ - agent_name: string; - /** - * Controls - * @description Per-control breakdown - */ - controls: components['schemas']['ControlStats'][]; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** @description Agent-level aggregate statistics */ - totals: components['schemas']['StatsTotals']; - }; - /** - * StatsTotals - * @description Agent-level aggregate statistics. - * - * Invariant: execution_count = match_count + non_match_count + error_count - * - * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. - * sum(action_counts.values()) == match_count - * - * Attributes: - * execution_count: Total executions across all controls - * match_count: Total matches across all controls (evaluator matched) - * non_match_count: Total non-matches across all controls (evaluator didn't match) - * error_count: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions - * timeseries: Time-series data points (only when include_timeseries=true) - */ - StatsTotals: { - /** - * Action Counts - * @description Action breakdown for matches: {allow, deny, steer, warn, log} - */ - action_counts?: { - [key: string]: number; - }; - /** - * Error Count - * @description Total errors - * @default 0 - */ - error_count: number; - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Match Count - * @description Total matches - * @default 0 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - * @default 0 - */ - non_match_count: number; - /** - * Timeseries - * @description Time-series data points (only when include_timeseries=true) - */ - timeseries?: components['schemas']['TimeseriesBucket'][] | null; - }; - /** - * Step - * @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 Output content for this step (None for pre-checks) */ - output?: components['schemas']['JSONValue'] | null; - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * StepKey - * @description Identifies a registered step schema by type and name. - */ - StepKey: { - /** - * Name - * @description Registered step name - */ - name: string; - /** - * Type - * @description Step type - */ - type: string; - }; - /** - * StepSchema - * @description Schema for a registered agent step. - * @example { - * "description": "Search the internal knowledge base", - * "input_schema": { - * "query": { - * "description": "Search query", - * "type": "string" - * } - * }, - * "name": "search_knowledge_base", - * "output_schema": { - * "results": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "type": "tool" - * } - * @example { - * "description": "Customer support response generation", - * "input_schema": { - * "messages": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "name": "support-answer", - * "output_schema": { - * "text": { - * "type": "string" - * } - * }, - * "type": "llm" - * } - */ - StepSchema: { - /** - * Description - * @description Optional description of the step - */ - description?: string | null; - /** - * Input Schema - * @description JSON schema describing step input - */ - 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 - */ - output_schema?: { - [key: string]: unknown; - } | null; - /** - * Type - * @description Step type for this schema (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * TimeseriesBucket - * @description Single data point in a time-series. - * - * Represents aggregated metrics for a single time bucket. - * - * Attributes: - * timestamp: Start time of the bucket (UTC, always timezone-aware) - * execution_count: Total executions in this bucket - * match_count: Number of matches in this bucket - * non_match_count: Number of non-matches in this bucket - * error_count: Number of errors in this bucket - * action_counts: Breakdown of actions for matched executions - * avg_confidence: Average confidence score (None if no executions) - * avg_duration_ms: Average execution duration in milliseconds (None if no data) - */ - TimeseriesBucket: { - /** - * Action Counts - * @description Action breakdown: {allow, deny, steer, warn, log} - */ - 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 - */ - execution_count: number; - /** - * Match Count - * @description Matches in bucket - */ - match_count: number; - /** - * Non Match Count - * @description Non-matches in bucket - */ - non_match_count: number; - /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) - */ - timestamp: string; - }; - /** - * UpdateEvaluatorConfigRequest - * @description Request to replace an evaluator config template. - */ - UpdateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - 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 */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * Agent + * @description Agent metadata for registration and tracking. + * + * An agent represents an AI system that can be protected and monitored. + * 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_metadata": { + * "environment": "production", + * "team": "support" + * }, + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * } + */ + Agent: { + /** + * Agent Created At + * @description ISO 8601 timestamp when agent was created + */ + agent_created_at?: string | null; + /** + * Agent Description + * @description Optional description of the agent's purpose + */ + agent_description?: string | null; + /** + * Agent Metadata + * @description Free-form metadata dictionary for custom properties + */ + 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 + */ + agent_updated_at?: string | null; + /** + * Agent Version + * @description Semantic version string (e.g. '1.0.0') + */ + agent_version?: string | null; + }; + /** AgentControlsResponse */ + AgentControlsResponse: { + /** + * Controls + * @description List of active controls associated with the agent + */ + controls: components["schemas"]["Control"][]; + }; + /** + * AgentRef + * @description Reference to an agent (for listing which agents use a control). + */ + AgentRef: { + /** + * Agent Name + * @description Agent name + */ + agent_name: string; + }; + /** + * AgentSummary + * @description Summary of an agent for list responses. + */ + AgentSummary: { + /** + * Active Controls Count + * @description Number of active controls for this agent + * @default 0 + */ + active_controls_count: number; + /** + * Agent Name + * @description Unique identifier of the agent + */ + agent_name: string; + /** + * Created At + * @description ISO 8601 timestamp when agent was created + */ + created_at?: string | null; + /** + * Evaluator Count + * @description Number of evaluators registered with the agent + * @default 0 + */ + evaluator_count: number; + /** + * Policy Id + * @description Deprecated: first associated policy ID, if any + */ + policy_id?: number | null; + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + /** + * Step Count + * @description Number of steps registered with the agent + * @default 0 + */ + step_count: number; + }; + /** AssocResponse */ + AssocResponse: { + /** + * Success + * @description Whether the association change succeeded + */ + success: boolean; + }; + /** + * BatchEventsRequest + * @description Request model for batch event ingestion. + * + * SDKs batch events and send them to the server periodically. + * This reduces HTTP overhead significantly (100x reduction). + * + * Attributes: + * events: List of control execution events to ingest + * @example { + * "events": [ + * { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_id": 123, + * "control_name": "sql-injection-check", + * "matched": true, + * "span_id": "00f067aa0ba902b7", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * ] + * } + */ + BatchEventsRequest: { + /** + * Events + * @description List of events to ingest + */ + events: components["schemas"]["ControlExecutionEvent"][]; + }; + /** + * BatchEventsResponse + * @description Response model for batch event ingestion. + * + * Attributes: + * received: Number of events received + * enqueued: Number of events successfully enqueued + * dropped: Number of events dropped (queue full) + * status: Overall status ('queued', 'partial', 'failed') + */ + BatchEventsResponse: { + /** + * Dropped + * @description Number of events dropped + */ + dropped: number; + /** + * Enqueued + * @description Number of events enqueued + */ + enqueued: number; + /** + * Received + * @description Number of events received + */ + received: number; + /** + * Status + * @description Overall ingestion status + * @enum {string} + */ + 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. + * + * Note: Only fully-configured controls (with valid ControlDefinition) + * are returned from API endpoints. Unconfigured controls are filtered out. + */ + Control: { + control: components["schemas"]["ControlDefinition-Output"]; + /** Id */ + id: number; + /** Name */ + name: string; + }; + /** + * ControlAction + * @description What to do when control matches. + */ + ControlAction: { + /** + * Decision + * @description Action to take when control is triggered + * @enum {string} + */ + decision: "allow" | "deny" | "steer" | "warn" | "log"; + /** @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. */ + steering_context?: components["schemas"]["SteeringContext"] | null; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + "ControlDefinition-Input": { + /** @description What action to take when control matches */ + action: components["schemas"]["ControlAction"]; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components["schemas"]["EvaluatorSpec"]; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: "server" | "sdk"; + /** @description Which steps and stages this control applies to */ + scope?: components["schemas"]["ControlScope"]; + /** @description What data to select from the payload */ + selector: components["schemas"]["ControlSelector"]; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + "ControlDefinition-Output": { + /** @description What action to take when control matches */ + action: components["schemas"]["ControlAction"]; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components["schemas"]["EvaluatorSpec"]; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: "server" | "sdk"; + /** @description Which steps and stages this control applies to */ + scope?: components["schemas"]["ControlScope"]; + /** @description What data to select from the payload */ + selector: components["schemas"]["ControlSelector"]; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlExecutionEvent + * @description Represents a single control execution event. + * + * This is the core observability data model, capturing: + * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) + * - Context: agent, control, check stage, applies to + * - Result: action taken, whether matched, confidence score + * - Timing: when it happened, how long it took + * - Optional details: evaluator name, selector path, errors, metadata + * + * Attributes: + * 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_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) + * applies_to: "llm_call" or "tool_call" + * action: The action taken (allow, deny, warn, log) + * matched: Whether the control evaluator matched + * confidence: Confidence score from the evaluator (0.0-1.0) + * timestamp: When the control was executed (UTC) + * execution_duration_ms: How long the control evaluation took + * evaluator_name: Name of the evaluator used + * selector_path: The selector path used to extract data + * error_message: Error message if evaluation failed + * metadata: Additional metadata for extensibility + * @example { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", + * "control_id": 123, + * "control_name": "sql-injection-check", + * "evaluator_name": "regex", + * "execution_duration_ms": 15.3, + * "matched": true, + * "selector_path": "input", + * "span_id": "00f067aa0ba902b7", + * "timestamp": "2025-01-09T10:30:00Z", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + */ + ControlExecutionEvent: { + /** + * Action + * @description Action taken by the control + * @enum {string} + */ + action: "allow" | "deny" | "steer" | "warn" | "log"; + /** + * Agent Name + * @description Identifier of the agent + */ + agent_name: string; + /** + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} + */ + applies_to: "llm_call" | "tool_call"; + /** + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} + */ + check_stage: "pre" | "post"; + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control (denormalized) + */ + control_name: string; + /** + * Error Message + * @description Error message if evaluation failed + */ + error_message?: string | null; + /** + * Evaluator Name + * @description Name of the evaluator used + */ + evaluator_name?: string | null; + /** + * Execution Duration Ms + * @description Execution duration in milliseconds + */ + execution_duration_ms?: number | null; + /** + * Matched + * @description Whether the evaluator matched (True) or not (False) + */ + matched: boolean; + /** + * Metadata + * @description Additional metadata + */ + metadata?: { + [key: string]: unknown; + }; + /** + * Selector Path + * @description Selector path used to extract data + */ + selector_path?: string | null; + /** + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + */ + span_id: string; + /** + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) + */ + 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" | "steer" | "warn" | "log"; + /** + * Control Execution Id + * @description Unique ID for this control execution (generated by engine) + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control + */ + control_name: string; + /** @description Evaluator result (confidence, message, metadata) */ + result: components["schemas"]["EvaluatorResult"]; + /** @description Steering context for steer actions if configured */ + steering_context?: components["schemas"]["SteeringContext"] | null; + }; + /** + * ControlScope + * @description Defines when a control applies to a Step. + * @example { + * "stages": [ + * "pre" + * ], + * "step_types": [ + * "tool" + * ] + * } + * @example { + * "step_names": [ + * "search_db", + * "fetch_user" + * ] + * } + * @example { + * "step_name_regex": "^db_.*" + * } + * @example { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * } + */ + ControlScope: { + /** + * Stages + * @description Evaluation stages this control applies to + */ + stages?: ("pre" | "post")[] | null; + /** + * Step Name Regex + * @description RE2 pattern matched with search() against step name + */ + step_name_regex?: string | null; + /** + * Step Names + * @description Exact step names this control applies to + */ + 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 + * @description Selects data from a Step payload. + * + * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" + * meaning the entire Step object. + * @example { + * "path": "output" + * } + * @example { + * "path": "context.user_id" + * } + * @example { + * "path": "input" + * } + * @example { + * "path": "*" + * } + * @example { + * "path": "name" + * } + * @example { + * "path": "output" + * } + */ + ControlSelector: { + /** + * Path + * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' + * @default * + */ + path: string | null; + }; + /** + * ControlStats + * @description Aggregated statistics for a single control. + * + * Attributes: + * control_id: Database ID of the control + * control_name: Name of the control + * execution_count: Total number of executions + * match_count: Number of times the control matched + * non_match_count: Number of times the control did not match + * allow_count: Number of allow actions + * deny_count: Number of deny actions + * steer_count: Number of steer actions + * warn_count: Number of warn actions + * log_count: Number of log actions + * error_count: Number of errors during evaluation + * avg_confidence: Average confidence score + * 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 + */ + control_id: number; + /** + * Control Name + * @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 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + */ + non_match_count: number; + /** + * Steer Count + * @description Steer actions + */ + steer_count: number; + /** + * Warn Count + * @description Warn actions + */ + warn_count: number; + }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_name: Agent identifier + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Name + * @description Agent identifier + */ + agent_name: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + 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: { + /** + * Description + * @description Control description + */ + description?: string | null; + /** + * Enabled + * @description Whether control is enabled + * @default true + */ + enabled: boolean; + /** + * Execution + * @description 'server' or 'sdk' + */ + execution?: string | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + /** + * Stages + * @description Evaluation stages in scope + */ + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; + /** + * Tags + * @description Control tags + */ + tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components["schemas"]["AgentRef"] | null; + /** + * Used By Agents Count + * @description Number of unique agents using this control + * @default 0 + */ + used_by_agents_count: number; + }; + /** CreateControlRequest */ + CreateControlRequest: { + /** + * Name + * @description Unique control name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreateControlResponse */ + CreateControlResponse: { + /** + * Control Id + * @description Identifier of the created control + */ + control_id: number; + }; + /** + * CreateEvaluatorConfigRequest + * @description Request to create an evaluator config template. + */ + CreateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyRequest */ + CreatePolicyRequest: { + /** + * Name + * @description Unique policy name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyResponse */ + CreatePolicyResponse: { + /** + * Policy Id + * @description Identifier of the created policy + */ + policy_id: number; + }; + /** + * DeleteControlResponse + * @description Response for deleting a control. + */ + DeleteControlResponse: { + /** + * Dissociated From + * @description Deprecated: policy IDs the control was removed from before deletion + */ + dissociated_from?: number[]; + /** + * Dissociated From Agents + * @description Agent names the control was removed from before deletion + */ + dissociated_from_agents?: string[]; + /** + * Dissociated From Policies + * @description Policy IDs the control was removed from before deletion + */ + dissociated_from_policies?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; + }; + /** + * DeleteEvaluatorConfigResponse + * @description Response for deleting an evaluator config. + */ + DeleteEvaluatorConfigResponse: { + /** + * Success + * @description Whether the evaluator config was deleted + */ + success: boolean; + }; + /** + * DeletePolicyResponse + * @description Compatibility response for singular policy deletion endpoint. + */ + DeletePolicyResponse: { + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * EvaluationRequest + * @description Request model for evaluation analysis. + * + * Used to analyze agent interactions for safety violations, + * policy compliance, and control rules. + * + * Attributes: + * 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_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "output": "I cannot share sensitive payment information.", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "type": "tool" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "output": { + * "results": [] + * }, + * "type": "tool" + * } + * } + */ + EvaluationRequest: { + /** + * Agent Name + * @description Identifier of the agent making the evaluation request + */ + 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 + * @description Response model from evaluation analysis (server-side). + * + * This is what the server returns. The SDK may transform this + * into an EvaluationResult for client convenience. + * + * Attributes: + * is_safe: Whether the content is considered safe + * confidence: Confidence score between 0.0 and 1.0 + * reason: Optional explanation for the decision + * matches: List of controls that matched/triggered (if any) + * errors: List of controls that failed during evaluation (if any) + * non_matches: List of controls that were evaluated but did not match (if any) + */ + EvaluationResponse: { + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Errors + * @description List of controls that failed during evaluation (if any) + */ + 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; + /** + * Non Matches + * @description List of controls that were evaluated but did not match (if any) + */ + non_matches?: components["schemas"]["ControlMatch"][] | null; + /** + * Reason + * @description Explanation for the decision + */ + reason?: string | null; + }; + /** + * EvaluatorConfigItem + * @description Evaluator config template stored in the server. + */ + EvaluatorConfigItem: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Created At + * @description ISO 8601 created timestamp + */ + created_at?: string | null; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Id + * @description Evaluator config ID + */ + id: number; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + /** + * Updated At + * @description ISO 8601 updated timestamp + */ + updated_at?: string | null; + }; + /** + * EvaluatorInfo + * @description Information about a registered evaluator. + */ + EvaluatorInfo: { + /** + * Config Schema + * @description JSON Schema for config + */ + 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 + */ + requires_api_key: boolean; + /** + * Timeout Ms + * @description Default timeout in milliseconds + */ + timeout_ms: number; + /** + * Version + * @description Evaluator version + */ + version: string; + }; + /** + * EvaluatorResult + * @description Result from a control evaluator. + * + * The `error` field indicates evaluator failures, NOT validation failures: + * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors + * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures + * + * When `error` is set, `matched` must be False (fail-open on evaluator errors). + * When `error` is None, `matched` reflects the actual validation result. + * + * This distinction allows: + * - Clients to distinguish "data violated rules" from "evaluator is broken" + * - Observability systems to monitor evaluator health separately from validation outcomes + */ + EvaluatorResult: { + /** + * 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 + */ + message?: string | null; + /** + * Metadata + * @description Additional result metadata + */ + metadata?: { + [key: string]: unknown; + } | null; + }; + /** + * EvaluatorSchema + * @description Schema for a custom evaluator registered with an agent. + * + * Custom evaluators are Evaluator classes deployed with the engine. + * This schema is registered via initAgent for validation and UI purposes. + */ + EvaluatorSchema: { + /** + * Config Schema + * @description JSON Schema for evaluator config validation + */ + config_schema?: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; + }; + /** + * EvaluatorSchemaItem + * @description Evaluator schema summary for list response. + */ + EvaluatorSchemaItem: { + /** 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 + * @description Request model for querying raw events. + * + * Supports filtering by various criteria and pagination. + * + * Attributes: + * 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_name: Filter by agent identifier + * control_ids: Filter by control IDs + * actions: Filter by actions (allow, deny, steer, 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_name": "my-agent", + * "limit": 50, + * "start_time": "2025-01-09T00:00:00Z" + * } + */ + EventQueryRequest: { + /** + * Actions + * @description Filter by actions + */ + actions?: ("allow" | "deny" | "steer" | "warn" | "log")[] | null; + /** + * Agent Name + * @description Filter by agent identifier + */ + 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; + /** + * Control Execution Id + * @description Filter by specific event ID + */ + control_execution_id?: string | null; + /** + * Control Ids + * @description Filter by control IDs + */ + control_ids?: number[] | null; + /** + * End Time + * @description Filter events before this time + */ + end_time?: string | null; + /** + * Limit + * @description Maximum events + * @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 + * @description Response model for event queries. + * + * Attributes: + * events: List of matching events + * total: Total number of matching events (for pagination) + * limit: Limit used in query + * offset: Offset used in query + */ + EventQueryResponse: { + /** + * Events + * @description Matching events + */ + events: components["schemas"]["ControlExecutionEvent"][]; + /** + * Limit + * @description Limit used in query + */ + limit: number; + /** + * Offset + * @description Offset used in query + */ + offset: number; + /** + * Total + * @description Total matching events + */ + total: number; + }; + /** GetAgentPoliciesResponse */ + GetAgentPoliciesResponse: { + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + }; + /** + * GetAgentResponse + * @description Response containing agent details and registered steps. + */ + GetAgentResponse: { + /** @description Agent metadata */ + agent: components["schemas"]["Agent"]; + /** + * 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: { + /** @description Control data payload */ + data: components["schemas"]["ControlDefinition-Output"]; + }; + /** + * GetControlResponse + * @description Response containing control details. + */ + GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components["schemas"]["ControlDefinition-Output"] | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + }; + /** + * GetPolicyControlsResponse + * @description Response containing control IDs associated with a policy. + */ + GetPolicyControlsResponse: { + /** + * Control Ids + * @description List of control IDs associated with the policy + */ + control_ids: number[]; + }; + /** + * GetPolicyResponse + * @description Compatibility response for singular policy retrieval endpoint. + */ + GetPolicyResponse: { + /** + * Policy Id + * @description Associated policy ID + */ + policy_id: number; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** + * HealthResponse + * @description Health check response model. + * + * Attributes: + * status: Current health status (e.g., "healthy", "degraded", "unhealthy") + * version: Application version + */ + HealthResponse: { + /** Status */ + status: string; + /** 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_name": "customer-service-bot", + * "agent_version": "1.0.0" + * }, + * "evaluators": [ + * { + * "config_schema": { + * "properties": { + * "sensitivity": { + * "type": "string" + * } + * }, + * "type": "object" + * }, + * "description": "Detects PII in text", + * "name": "pii-detector" + * } + * ], + * "steps": [ + * { + * "input_schema": { + * "query": { + * "type": "string" + * } + * }, + * "name": "search_kb", + * "output_schema": { + * "results": { + * "type": "array" + * } + * }, + * "type": "tool" + * } + * ] + * } + */ + InitAgentRequest: { + /** @description Agent metadata including ID, name, and version */ + agent: components["schemas"]["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 + */ + conflict_mode: components["schemas"]["ConflictMode"]; + /** + * Evaluators + * @description Custom evaluator schemas for config validation + */ + evaluators?: components["schemas"]["EvaluatorSchema"][]; + /** + * Force Replace + * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. + * @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 + */ + controls?: components["schemas"]["Control"][]; + /** + * Created + * @description True if agent was newly created, False if updated + */ + created: boolean; + /** + * Overwrite Applied + * @description True if overwrite mode changed registration data on an existing agent + * @default false + */ + overwrite_applied: boolean; + /** @description Detailed list of changes applied in overwrite mode */ + overwrite_changes?: components["schemas"]["InitAgentOverwriteChanges"]; + }; + JSONObject: { + [key: string]: components["schemas"]["JSONValue"]; + }; + /** @description Any JSON value */ + JSONValue: unknown; + /** + * ListAgentsResponse + * @description Response for listing agents. + */ + ListAgentsResponse: { + /** + * Agents + * @description List of agent summaries + */ + agents: components["schemas"]["AgentSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListControlsResponse + * @description Response for listing controls. + */ + ListControlsResponse: { + /** + * Controls + * @description List of control summaries + */ + controls: components["schemas"]["ControlSummary"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorConfigsResponse + * @description Response for listing evaluator configs. + */ + ListEvaluatorConfigsResponse: { + /** + * Evaluator Configs + * @description List of evaluator configs + */ + evaluator_configs: components["schemas"]["EvaluatorConfigItem"][]; + /** @description Pagination metadata */ + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * ListEvaluatorsResponse + * @description Response for listing agent's evaluator schemas. + */ + ListEvaluatorsResponse: { + /** Evaluators */ + evaluators: components["schemas"]["EvaluatorSchemaItem"][]; + pagination: components["schemas"]["PaginationInfo"]; + }; + /** + * PaginationInfo + * @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; + /** + * Next Cursor + * @description Cursor for fetching the next page (null if no more pages) + */ + next_cursor?: string | null; + /** + * Total + * @description Total number of items + */ + total: number; + }; + /** + * PatchAgentRequest + * @description Request to modify an agent (remove steps/evaluators). + */ + PatchAgentRequest: { + /** + * 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: { + /** + * 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: { + /** + * Enabled + * @description Enable or disable the control + */ + enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; + }; + /** + * PatchControlResponse + * @description Response from control metadata update. + */ + PatchControlResponse: { + /** + * Enabled + * @description Current enabled status (if control has data configured) + */ + enabled?: boolean | null; + /** + * Name + * @description Current control name (may have changed) + */ + name: string; + /** + * Success + * @description Whether the update succeeded + */ + success: boolean; + }; + /** + * RemoveAgentControlResponse + * @description Response for removing a direct agent-control association. + */ + RemoveAgentControlResponse: { + /** + * Control Still Active + * @description True if the control remains active via policy association(s) + */ + control_still_active: boolean; + /** + * Removed Direct Association + * @description True if a direct agent-control link was removed + */ + removed_direct_association: boolean; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * SetControlDataRequest + * @description Request to update control configuration data. + */ + SetControlDataRequest: { + /** @description Control configuration data (replaces existing) */ + data: components["schemas"]["ControlDefinition-Input"]; + }; + /** SetControlDataResponse */ + SetControlDataResponse: { + /** + * Success + * @description Whether the control data was updated + */ + success: boolean; + }; + /** + * SetPolicyResponse + * @description Compatibility response for singular policy assignment endpoint. + */ + SetPolicyResponse: { + /** + * Old Policy Id + * @description Previously associated policy ID, if any + */ + old_policy_id?: number | null; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * StatsResponse + * @description Response model for agent-level aggregated statistics. + * + * Contains agent-level totals (with optional timeseries) and per-control breakdown. + * + * Attributes: + * 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 Name + * @description Agent identifier + */ + agent_name: string; + /** + * Controls + * @description Per-control breakdown + */ + controls: components["schemas"]["ControlStats"][]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components["schemas"]["StatsTotals"]; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * timeseries: Time-series data points (only when include_timeseries=true) + */ + StatsTotals: { + /** + * Action Counts + * @description Action breakdown for matches: {allow, deny, steer, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Error Count + * @description Total errors + * @default 0 + */ + error_count: number; + /** + * Execution Count + * @description Total executions + */ + execution_count: number; + /** + * Match Count + * @description Total matches + * @default 0 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + * @default 0 + */ + non_match_count: number; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components["schemas"]["TimeseriesBucket"][] | null; + }; + /** + * SteeringContext + * @description Steering context for steer actions. + * + * This model provides an extensible structure for steering guidance. + * Future fields could include severity, categories, suggested_actions, etc. + * @example { + * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." + * } + * @example { + * "message": "Transfer exceeds daily limit. Steps: 1) Ask user for business justification, 2) Request manager approval with amount and justification, 3) If approved, retry with manager_approved=True and justification filled in." + * } + */ + SteeringContext: { + /** + * Message + * @description Guidance message explaining what needs to be corrected and how + */ + message: string; + }; + /** + * Step + * @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 Output content for this step (None for pre-checks) */ + output?: components["schemas"]["JSONValue"] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * StepKey + * @description Identifies a registered step schema by type and name. + */ + StepKey: { + /** + * Name + * @description Registered step name + */ + name: string; + /** + * Type + * @description Step type + */ + type: string; + }; + /** + * StepSchema + * @description Schema for a registered agent step. + * @example { + * "description": "Search the internal knowledge base", + * "input_schema": { + * "query": { + * "description": "Search query", + * "type": "string" + * } + * }, + * "name": "search_knowledge_base", + * "output_schema": { + * "results": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "type": "tool" + * } + * @example { + * "description": "Customer support response generation", + * "input_schema": { + * "messages": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "name": "support-answer", + * "output_schema": { + * "text": { + * "type": "string" + * } + * }, + * "type": "llm" + * } + */ + StepSchema: { + /** + * Description + * @description Optional description of the step + */ + description?: string | null; + /** + * Input Schema + * @description JSON schema describing step input + */ + 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 + */ + output_schema?: { + [key: string]: unknown; + } | null; + /** + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * Action Counts + * @description Action breakdown: {allow, deny, steer, warn, log} + */ + 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 + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + }; + /** + * UpdateEvaluatorConfigRequest + * @description Request to replace an evaluator config template. + */ + UpdateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** + * ValidateControlDataRequest + * @description Request to validate control configuration data without saving. + */ + ValidateControlDataRequest: { + /** @description Control configuration data to validate */ + data: components["schemas"]["ControlDefinition-Input"]; + }; + /** 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 */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - list_agents_api_v1_agents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - name?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of agent summaries */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListAgentsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - init_agent_api_v1_agents_initAgent_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['InitAgentRequest']; - }; - }; - responses: { - /** @description Agent registration status with active controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['InitAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_api_v1_agents__agent_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Agent metadata and registered steps */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_agent_api_v1_agents__agent_name__patch: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchAgentRequest']; - }; - }; - responses: { - /** @description Lists of removed items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchAgentResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_controls_api_v1_agents__agent_name__controls_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of controls from agent's policy */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AgentControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schemas registered with this agent */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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']; - }; - }; - }; - }; - get_agent_policy_api_v1_agents__agent_name__policy_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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_name__policy_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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']; - }; - }; - }; - }; - 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 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']; - }; - }; - }; - }; - list_controls_api_v1_controls_get: { - parameters: { - query?: { - /** @description Control ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by enabled status */ - enabled?: boolean | null; - /** @description Filter by step type (built-ins: 'tool', 'llm') */ - step_type?: string | null; - /** @description Filter by stage ('pre' or 'post') */ - stage?: string | null; - /** @description Filter by execution ('server' or 'sdk') */ - execution?: string | null; - /** @description Filter by tag */ - tag?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListControlsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_control_api_v1_controls_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateControlRequest']; - }; - }; - responses: { - /** @description Created control ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['CreateControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control metadata and configuration */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_control_api_v1_controls__control_id__delete: { - parameters: { - query?: { - /** @description If true, dissociate from all policies before deleting. If false, fail if control is associated with any policy. */ - force?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation with dissociation info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - patch_control_api_v1_controls__control_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['PatchControlRequest']; - }; - }; - responses: { - /** @description Updated control information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PatchControlResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_data_api_v1_controls__control_id__data_get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control data payload */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['GetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - set_control_data_api_v1_controls__control_id__data_put: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['SetControlDataRequest']; - }; - }; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SetControlDataResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - 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?: { - /** @description Evaluator config ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by evaluator name */ - evaluator?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of evaluator configs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ListEvaluatorConfigsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - create_evaluator_config_api_v1_evaluator_configs_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Created evaluator config */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_evaluator_config_api_v1_evaluator_configs__config_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator config details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - update_evaluator_config_api_v1_evaluator_configs__config_id__put: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateEvaluatorConfigRequest']; - }; - }; - responses: { - /** @description Updated evaluator config */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EvaluatorConfigItem']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['DeleteEvaluatorConfigResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_evaluators_api_v1_evaluators_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Dictionary of evaluator name to evaluator info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: components['schemas']['EvaluatorInfo']; - }; - }; - }; - }; - }; - ingest_events_api_v1_observability_events_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['BatchEventsRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BatchEventsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - query_events_api_v1_observability_events_query_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EventQueryRequest']; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['EventQueryResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_stats_api_v1_observability_stats_get: { - parameters: { - query: { - agent_name: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['StatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_control_stats_api_v1_observability_stats_controls__control_id__get: { - parameters: { - query: { - agent_name: string; - time_range?: - | '1m' - | '5m' - | '15m' - | '1h' - | '24h' - | '7d' - | '30d' - | '180d' - | '365d'; - include_timeseries?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ControlStatsResponse']; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HTTPValidationError']; - }; - }; - }; - }; - get_status_api_v1_observability_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - [key: string]: unknown; - }; - }; - }; - }; - }; - 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; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server health status */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['HealthResponse']; + list_agents_api_v1_agents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + name?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of agent summaries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListAgentsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + init_agent_api_v1_agents_initAgent_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["InitAgentRequest"]; + }; + }; + responses: { + /** @description Agent registration status with active controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InitAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_api_v1_agents__agent_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Agent metadata and registered steps */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_agent_api_v1_agents__agent_name__patch: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchAgentRequest"]; + }; + }; + responses: { + /** @description Lists of removed items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchAgentResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_agent_controls_api_v1_agents__agent_name__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of controls from agent policy and direct associations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AgentControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + add_agent_control_api_v1_agents__agent_name__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + 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_agent_control_api_v1_agents__agent_name__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RemoveAgentControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schemas registered with this agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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"]; + }; + }; + }; + }; + get_agent_policies_api_v1_agents__agent_name__policies_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of policy IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetAgentPoliciesResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + remove_all_agent_policies_api_v1_agents__agent_name__policies_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + 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"]; + }; + }; + }; + }; + add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_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_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_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"]; + }; + }; + }; + }; + get_agent_policy_api_v1_agents__agent_name__policy_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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_name__policy_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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"]; + }; + }; + }; + }; + 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 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"]; + }; + }; + }; + }; + list_controls_api_v1_controls_get: { + parameters: { + query?: { + /** @description Control ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by enabled status */ + enabled?: boolean | null; + /** @description Filter by step type (built-ins: 'tool', 'llm') */ + step_type?: string | null; + /** @description Filter by stage ('pre' or 'post') */ + stage?: string | null; + /** @description Filter by execution ('server' or 'sdk') */ + execution?: string | null; + /** @description Filter by tag */ + tag?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListControlsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_control_api_v1_controls_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateControlRequest"]; + }; + }; + responses: { + /** @description Created control ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreateControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + 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; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control metadata and configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_control_api_v1_controls__control_id__delete: { + parameters: { + query?: { + /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + force?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation with dissociation info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + patch_control_api_v1_controls__control_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PatchControlRequest"]; + }; + }; + responses: { + /** @description Updated control information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchControlResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_data_api_v1_controls__control_id__data_get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control data payload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + set_control_data_api_v1_controls__control_id__data_put: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SetControlDataRequest"]; + }; + }; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetControlDataResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + 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?: { + /** @description Evaluator config ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by evaluator name */ + evaluator?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of evaluator configs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ListEvaluatorConfigsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + create_evaluator_config_api_v1_evaluator_configs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Created evaluator config */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluator_config_api_v1_evaluator_configs__config_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator config details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_evaluator_config_api_v1_evaluator_configs__config_id__put: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateEvaluatorConfigRequest"]; + }; + }; + responses: { + /** @description Updated evaluator config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EvaluatorConfigItem"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeleteEvaluatorConfigResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_evaluators_api_v1_evaluators_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dictionary of evaluator name to evaluator info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: components["schemas"]["EvaluatorInfo"]; + }; + }; + }; + }; + }; + ingest_events_api_v1_observability_events_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BatchEventsRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BatchEventsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + query_events_api_v1_observability_events_query_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EventQueryRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EventQueryResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_stats_api_v1_observability_stats_get: { + parameters: { + query: { + agent_name: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_name: string; + time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ControlStatsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + get_status_api_v1_observability_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: unknown; + }; + }; + }; + }; + }; + 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; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server health status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; }; - }; }; - }; } diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index 1ff82d88..b61160e2 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -69,14 +69,18 @@ export type GetAgentResponse = components['schemas']['GetAgentResponse']; export type ControlActionDecision = components['schemas']['ControlAction']['decision']; export type ControlExecution = - components['schemas']['ControlDefinition']['execution']; + components['schemas']['ControlDefinition-Input']['execution']; export type ControlStage = NonNullable< components['schemas']['ControlScope']['stages'] >[number]; export type ControlScope = components['schemas']['ControlScope']; export type ControlSelector = components['schemas']['ControlSelector']; export type ControlAction = components['schemas']['ControlAction']; -export type ControlDefinition = components['schemas']['ControlDefinition']; +export type ControlDefinitionInput = + components['schemas']['ControlDefinition-Input']; +export type ControlDefinitionOutput = + components['schemas']['ControlDefinition-Output']; +export type ControlDefinition = ControlDefinitionInput | ControlDefinitionOutput; export type Control = components['schemas']['Control']; export type AgentControlsResponse = components['schemas']['AgentControlsResponse']; @@ -93,23 +97,15 @@ export type SetControlDataResponse = export type GetControlDataResponse = components['schemas']['GetControlDataResponse']; -// Validate control data types (not yet in generated schemas) -// TODO: replace these with generated types after running pnpm fetch-api-types -export type ValidateControlDataRequest = { - data: ControlDefinition; -}; -export type ValidateControlDataResponse = { - success: boolean; -}; +export type ValidateControlDataRequest = + components['schemas']['ValidateControlDataRequest']; +export type ValidateControlDataResponse = + components['schemas']['ValidateControlDataResponse']; export type ControlSummary = components['schemas']['ControlSummary']; 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_name: string; -}; +export type AgentRef = components['schemas']['AgentRef']; // Helper type to extract query parameters from operations type ExtractQueryParams = T extends { parameters: { query?: infer Q } } diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index f30397e5..98eaecd3 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -10,87 +10,12 @@ type AddControlToAgentParams = { definition: ControlDefinition; }; -function sanitizePolicyName(agentId: string) { - return `policy-${agentId}` - .toLowerCase() - .replace(/[^a-z0-9_-]/g, '-') - .slice(0, 255); -} - -async function ensureAgentPolicy(agentId: string): Promise { - const { - data: existingPolicy, - error: getPolicyError, - response: getPolicyResponse, - } = await api.agents.getPolicy(agentId); - - if (!getPolicyError && existingPolicy) { - return existingPolicy.policy_id; - } - - if (getPolicyResponse?.status !== 404) { - throw parseApiError( - getPolicyError, - 'Failed to fetch agent policy', - getPolicyResponse?.status - ); - } - - const policyNameBase = sanitizePolicyName(agentId); - const policyNameCandidates = [ - policyNameBase, - `${policyNameBase}-${Date.now()}`, - ]; - - let createdPolicyId: number | null = null; - for (const candidate of policyNameCandidates) { - const { - data: createdPolicy, - error: createPolicyError, - response: createPolicyResponse, - } = await api.policies.create(candidate); - - if (!createPolicyError && createdPolicy) { - createdPolicyId = createdPolicy.policy_id; - break; - } - - if (createPolicyResponse?.status === 409) { - continue; - } - - throw parseApiError( - createPolicyError, - 'Failed to create policy for agent', - createPolicyResponse?.status - ); - } - - if (createdPolicyId === null) { - throw new Error('Unable to create a unique policy for this agent'); - } - - const { error: setPolicyError, response: setPolicyResponse } = - await api.agents.setPolicy(agentId, createdPolicyId); - - if (setPolicyError) { - throw parseApiError( - setPolicyError, - 'Failed to assign policy to agent', - setPolicyResponse?.status - ); - } - - return createdPolicyId; -} - /** * Mutation hook to add a control to an agent * Flow: * 1. Create the control * 2. Set control data (definition) - * 3. Ensure the agent has a policy - * 4. Add the control to that policy + * 3. Associate the control directly with the agent */ export function useAddControlToAgent() { const queryClient = useQueryClient(); @@ -135,17 +60,14 @@ export function useAddControlToAgent() { ); } - // Step 3: Ensure the agent has a policy. - const policyId = await ensureAgentPolicy(agentId); - - // Step 4: Add control to policy. + // Step 3: Associate control directly with the agent. const { error: associateError, response: associateResponse } = - await api.policies.addControl(policyId, createdControlId); + await api.agents.addControl(agentId, createdControlId); if (associateError) { throw parseApiError( associateError, - 'Failed to add control to agent policy', + 'Failed to add control to agent', associateResponse?.status ); } diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-delete-control.ts index d0efba0f..93663216 100644 --- a/ui/src/core/hooks/query-hooks/use-delete-control.ts +++ b/ui/src/core/hooks/query-hooks/use-delete-control.ts @@ -10,8 +10,8 @@ type RemoveControlFromAgentParams = { export type RemoveControlFromAgentResult = { success: boolean; - removed_from_policy?: boolean; - no_policy_assigned?: boolean; + removed_direct_association: boolean; + control_still_active: boolean; }; /** @@ -25,40 +25,23 @@ export function useRemoveControlFromAgent() { agentId, controlId, }: RemoveControlFromAgentParams) => { - const { - data: policyData, - error: policyError, - response: policyResponse, - } = await api.agents.getPolicy(agentId); - - if (policyResponse?.status === 404) { - return { success: true, no_policy_assigned: true }; - } - - if (policyError || !policyData) { - throw parseApiError( - policyError, - 'Failed to fetch agent policy', - policyResponse?.status - ); - } - - const { data, error, response } = await api.policies.removeControl( - policyData.policy_id, + const { data, error, response } = await api.agents.removeControl( + agentId, controlId ); - if (error) { + if (error || !data) { throw parseApiError( error, - 'Failed to remove control from agent policy', + 'Failed to remove control from agent', response?.status ); } return { - success: data?.success ?? true, - removed_from_policy: true, + success: data.success, + removed_direct_association: data.removed_direct_association, + control_still_active: data.control_still_active, } satisfies RemoveControlFromAgentResult; }, onSuccess: (_data, variables) => { diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index c4100fe7..928bd830 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -27,7 +27,7 @@ export function useDeleteControlFlow({ children: ( Remove "{control.name}" from this agent? This only removes - it from the agent's assigned policy and does not delete the + the direct association from this agent and does not delete the control globally. ), @@ -46,18 +46,22 @@ export function useDeleteControlFlow({ }, { onSuccess: (result: RemoveControlFromAgentResult) => { - if (result.no_policy_assigned) { + if (!result.removed_direct_association) { notifications.show({ - title: 'No policy assigned', - message: `"${control.name}" was not removed because this agent has no assigned policy.`, - color: 'yellow', + title: 'No direct association found', + message: result.control_still_active + ? `"${control.name}" is still active for this agent through policy associations.` + : `"${control.name}" was not directly associated with this agent.`, + color: result.control_still_active ? 'yellow' : 'blue', }); return; } notifications.show({ title: 'Control removed', - message: `"${control.name}" has been removed from this agent policy.`, + message: result.control_still_active + ? `"${control.name}" was removed from direct associations, but remains active via policy associations.` + : `"${control.name}" has been removed from this agent.`, color: 'green', }); if (selectedControl?.id === control.id) { diff --git a/ui/tests/fixtures.ts b/ui/tests/fixtures.ts index 1eb14a23..39f0aa86 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -22,6 +22,7 @@ const agentsList: AgentSummary[] = [ { agent_name: 'customer-support-bot', policy_id: 1, + policy_ids: [1], created_at: '2024-01-01T00:00:00Z', step_count: 5, evaluator_count: 2, @@ -30,6 +31,7 @@ const agentsList: AgentSummary[] = [ { agent_name: 'data-analysis-agent', policy_id: 2, + policy_ids: [2], created_at: '2024-01-02T00:00:00Z', step_count: 3, evaluator_count: 1, @@ -38,6 +40,7 @@ const agentsList: AgentSummary[] = [ { agent_name: 'code-review-assistant', policy_id: 3, + policy_ids: [3], created_at: '2024-01-03T00:00:00Z', step_count: 8, evaluator_count: 4, @@ -156,6 +159,7 @@ const controlSummariesList: (ControlSummary & { stages: ['post'], tags: ['pii', 'compliance'], used_by_agent: { agent_name: 'customer-support-bot' }, + used_by_agents_count: 1, }, { id: 2, @@ -167,6 +171,7 @@ const controlSummariesList: (ControlSummary & { stages: ['pre'], tags: ['security'], used_by_agent: { agent_name: 'data-analysis-agent' }, + used_by_agents_count: 1, }, { id: 3, @@ -178,6 +183,7 @@ const controlSummariesList: (ControlSummary & { stages: ['pre'], tags: [], used_by_agent: null, + used_by_agents_count: 0, }, ]; From 4499d30b4fddb577eb103cc75e8e5dff26234c92 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 16:04:09 -0800 Subject: [PATCH 24/32] fix: restore python convenience associations and demo reset semantics --- examples/customer_support_agent/run_demo.py | 45 ++++++++- sdks/python/src/agent_control/__init__.py | 100 +++++++++++++++++- sdks/python/tests/test_integration_agents.py | 101 ++++++++++++++++++- 3 files changed, 237 insertions(+), 9 deletions(-) diff --git a/examples/customer_support_agent/run_demo.py b/examples/customer_support_agent/run_demo.py index a80327a3..773d1119 100644 --- a/examples/customer_support_agent/run_demo.py +++ b/examples/customer_support_agent/run_demo.py @@ -31,7 +31,7 @@ import os import sys -from agent_control import AgentControlClient, agents +from agent_control import AgentControlClient, agents, controls # Configure logging to see SDK debug output logging.basicConfig( @@ -59,7 +59,7 @@ async def reset_agent(): - """Reset the agent by removing all policy associations.""" + """Reset the agent by removing policy and direct control associations.""" agent_name = AGENT_ID server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -95,8 +95,47 @@ async def reset_agent(): print(f"Error removing policies: {e}") return + # Remove direct control associations (idempotent per control ID). + removed_direct_associations = 0 + cursor: int | None = None + while True: + controls_page = await controls.list_controls(client, cursor=cursor, limit=100) + control_summaries = controls_page.get("controls", []) + + for summary in control_summaries: + control_id = summary.get("id") + if control_id is None: + continue + try: + remove_result = await agents.remove_agent_control( + client, + agent_name, + control_id, + ) + if remove_result.get("removed_direct_association"): + removed_direct_associations += 1 + except Exception as e: + # Ignore transient 404s while iterating controls; keep best-effort cleanup. + if "404" not in str(e): + logger.debug( + "Error removing direct control %s from %s: %s", + control_id, + agent_name, + e, + ) + + pagination = controls_page.get("pagination", {}) + next_cursor = pagination.get("next_cursor") + if next_cursor is None: + break + cursor = int(next_cursor) + + print( + f"Removed {removed_direct_associations} direct control association(s) from agent." + ) + print() - print("Reset complete. The agent now has no policy-derived controls.") + print("Reset complete. The agent now has no policy or direct control associations.") print("Run the demo again and add controls via the UI to test.") diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index 621c9a08..00d7dd70 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -627,7 +627,7 @@ async def list_agents( Returns: Dictionary containing: - agents: List of agent summaries with agent_name, - policy_id, created_at, step_count, evaluator_count + policy_ids, created_at, step_count, evaluator_count - pagination: Object with limit, total, next_cursor, has_more Raises: @@ -656,6 +656,87 @@ async def main(): return await agents.list_agents(client, cursor=cursor, limit=limit) +# ============================================================================ +# Agent Association Convenience Functions +# ============================================================================ + + +async def get_agent_policies( + agent_name: str, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """List policy IDs associated with an agent.""" + _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_policies(client, agent_name) + + +async def add_agent_policy( + agent_name: str, + policy_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """Associate a policy with an agent (additive, idempotent).""" + _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.add_agent_policy(client, agent_name, policy_id) + + +async def remove_agent_policy_association( + agent_name: str, + policy_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """Remove one policy association from an agent (idempotent).""" + _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.remove_agent_policy_association(client, agent_name, policy_id) + + +async def remove_all_agent_policies( + agent_name: str, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """Remove all policy associations from an agent.""" + _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.remove_all_agent_policies(client, agent_name) + + +async def add_agent_control( + agent_name: str, + control_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """Associate a control directly with an agent (idempotent).""" + _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.add_agent_control(client, agent_name, control_id) + + +async def remove_agent_control( + agent_name: str, + control_id: int, + server_url: str | None = None, + api_key: str | None = None, +) -> dict[str, Any]: + """Remove a direct control association from an agent (idempotent).""" + _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.remove_agent_control(client, agent_name, control_id) + + # ============================================================================ # Control Management Convenience Functions # ============================================================================ @@ -836,7 +917,7 @@ async def delete_control( """ Delete a control from the server. - By default, deletion fails if the control is associated with any policy. + By default, deletion fails if the control is associated with any policy or agent. Use force=True to automatically dissociate and delete. Args: @@ -848,7 +929,8 @@ async def delete_control( Returns: Dictionary containing: - success: True if control was deleted - - dissociated_from: List of policy IDs the control was removed from + - dissociated_from_policies: List of policy IDs the control was removed from + - dissociated_from_agents: List of agent names the control was removed from Raises: httpx.HTTPError: If request fails @@ -862,7 +944,11 @@ async def delete_control( async def main(): # Force delete result = await agent_control.delete_control(5, force=True) - print(f"Deleted, removed from {len(result['dissociated_from'])} policies") + print( + "Deleted, removed from " + f"{len(result['dissociated_from_policies'])} policies and " + f"{len(result['dissociated_from_agents'])} agents" + ) asyncio.run(main()) """ @@ -1066,6 +1152,12 @@ async def main(): # Agent management "get_agent", "list_agents", + "get_agent_policies", + "add_agent_policy", + "remove_agent_policy_association", + "remove_all_agent_policies", + "add_agent_control", + "remove_agent_control", # Control management "create_control", "list_controls", diff --git a/sdks/python/tests/test_integration_agents.py b/sdks/python/tests/test_integration_agents.py index ee8a9017..66a22c43 100644 --- a/sdks/python/tests/test_integration_agents.py +++ b/sdks/python/tests/test_integration_agents.py @@ -9,9 +9,8 @@ import uuid -import pytest - import agent_control +import pytest from agent_control_models.server import AgentControlsResponse @@ -197,6 +196,104 @@ async def test_convenience_get_agent_function( print("✓ Convenience function works") +@pytest.mark.asyncio +async def test_convenience_agent_association_functions( + test_agent: dict, + test_policy: dict, + test_control: dict, + server_url: str, + api_key: str | None, +) -> None: + """Top-level convenience helpers support multi-policy and direct controls.""" + agent_name = test_agent["agent_name"] + policy_id = test_policy["policy_id"] + control_id = test_control["control_id"] + + control_data = { + "description": "Integration test control", + "enabled": True, + "execution": "server", + "scope": {"step_types": ["tool"], "stages": ["pre"]}, + "selector": {"path": "input"}, + "evaluator": { + "name": "regex", + "config": {"pattern": ".*"}, + }, + "action": {"decision": "allow"}, + "tags": ["test"], + } + + add_policy_result = await agent_control.add_agent_policy( + agent_name, + policy_id, + server_url=server_url, + api_key=api_key, + ) + assert add_policy_result["success"] is True + + policies_result = await agent_control.get_agent_policies( + agent_name, + server_url=server_url, + api_key=api_key, + ) + assert policy_id in policies_result["policy_ids"] + + add_control_result = await agent_control.add_agent_control( + agent_name, + control_id, + server_url=server_url, + api_key=api_key, + ) + assert add_control_result["success"] is True + + async with agent_control.AgentControlClient( + base_url=server_url, + api_key=api_key, + ) as client: + await agent_control.controls.set_control_data(client, control_id, control_data) + controls_result = await agent_control.agents.list_agent_controls(client, agent_name) + control_ids = {item["id"] for item in controls_result["controls"]} + assert control_id in control_ids + + remove_control_result = await agent_control.remove_agent_control( + agent_name, + control_id, + server_url=server_url, + api_key=api_key, + ) + assert remove_control_result["success"] is True + assert remove_control_result["removed_direct_association"] is True + + remove_policy_result = await agent_control.remove_agent_policy_association( + agent_name, + policy_id, + server_url=server_url, + api_key=api_key, + ) + assert remove_policy_result["success"] is True + + # Re-associate then clear all to verify remove_all convenience path. + await agent_control.add_agent_policy( + agent_name, + policy_id, + server_url=server_url, + api_key=api_key, + ) + clear_result = await agent_control.remove_all_agent_policies( + agent_name, + server_url=server_url, + api_key=api_key, + ) + assert clear_result["success"] is True + + policies_after_clear = await agent_control.get_agent_policies( + agent_name, + server_url=server_url, + api_key=api_key, + ) + assert policies_after_clear["policy_ids"] == [] + + @pytest.mark.asyncio async def test_init_function_workflow( test_agent_name: str, From 15eca02b1ef9647922d5f1cbe3a4446607c64be2 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 16:09:32 -0800 Subject: [PATCH 25/32] docs: align control association wording across sdk and examples --- examples/README.md | 4 +-- examples/agent_control_demo/demo_agent.py | 8 ++--- examples/agent_control_demo/setup_controls.py | 2 +- examples/customer_support_agent/README.md | 2 +- .../customer_support_agent/support_agent.py | 4 +-- examples/langchain/README.md | 4 +-- sdks/python/src/agent_control/__init__.py | 4 +-- .../src/agent_control/control_decorators.py | 29 +++++++++---------- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/examples/README.md b/examples/README.md index cb3ccf3f..7d22e475 100644 --- a/examples/README.md +++ b/examples/README.md @@ -126,13 +126,13 @@ See [steer_action_demo/README.md](steer_action_demo/README.md) for details. import agent_control from agent_control import control, ControlViolationError -# Initialize agent (connects to server, loads policy) +# Initialize agent (connects to server, loads control associations) agent_control.init( agent_name="my-bot", agent_description="My Bot", ) -# Apply the agent's assigned policy +# Apply controls associated with the agent @control() async def chat(message: str) -> str: return await assistant.respond(message) diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 14f37aab..13b9a5a8 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -105,12 +105,12 @@ async def execute_query(query: str) -> str: return result -@control() # Apply agent's assigned policy +@control() # Apply agent-associated controls async def process_request(input: str) -> str: """ - General processing function with the agent's policy applied. - - All controls in the policy are evaluated (both pre and post stage). + General processing function with agent-associated controls applied. + + Controls associated through policy or direct links are evaluated. """ print(f" [Agent] Processing request: {input}") response = simulate_llm_response(input) diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index 957a4844..c39a1bf4 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -487,7 +487,7 @@ async def main(): if not ok3: print("\n⚠️ Failed to assign policy to agent!") - # Verify: Get agent's policy + # Verify: Get agent policy associations print("\n Verifying agent policy assignment...") try: resp = await client.http_client.get( diff --git a/examples/customer_support_agent/README.md b/examples/customer_support_agent/README.md index 99d5d475..bd2d843a 100644 --- a/examples/customer_support_agent/README.md +++ b/examples/customer_support_agent/README.md @@ -167,7 +167,7 @@ agent_control.init( This: - Registers the agent with the server -- Fetches the assigned policy and controls +- Fetches controls associated with the agent - Enables the `@control()` decorator ### 2. Protecting Functions diff --git a/examples/customer_support_agent/support_agent.py b/examples/customer_support_agent/support_agent.py index 82273005..172cf559 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -26,7 +26,7 @@ # SDK INITIALIZATION # ============================================================================= # Call this once at the start of your application. -# The agent registers with the server and loads its assigned policy. +# The agent registers with the server and loads associated controls. agent_control.init( agent_name="646d5dea-c2e6-4453-b446-7035482b38e4", @@ -176,7 +176,7 @@ def create(cls, subject: str, description: str, priority: str = "medium") -> dic # PROTECTED AGENT FUNCTIONS # ============================================================================= # These functions are protected by the @control() decorator. -# The server evaluates controls and blocks/allows based on policy. +# The server evaluates controls and blocks/allows based on agent associations. @control() diff --git a/examples/langchain/README.md b/examples/langchain/README.md index 20513508..82a87e55 100644 --- a/examples/langchain/README.md +++ b/examples/langchain/README.md @@ -42,7 +42,7 @@ uv run setup_sql_controls.py This creates: - SQL safety control (blocks DROP, DELETE, TRUNCATE, ALTER, GRANT) - Policy with the control -- Assigns policy to the SQL agent +- Associates policy with the SQL agent > For local execution, create the control with `execution: "sdk"` in > `setup_sql_controls.py` (see `sql_control_data_sdk`) and enable @@ -155,7 +155,7 @@ cd server && make run **Causes:** 1. Server not running or evaluators not loaded (remote mode) -2. Control not assigned to agent's policy +2. Control not associated with the agent (policy or direct) 3. Control data missing/invalid (control not returned to agent) 4. Local mode enabled but control is still `execution: "server"` diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index 00d7dd70..5aba318c 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -375,10 +375,10 @@ def init( ] ) - # Now use @control decorator to apply the agent's policy + # Now use @control decorator to apply agent-associated controls from agent_control import control - @control() # Applies agent's assigned policy + @control() # Applies controls associated with the agent async def handle(message: str): return message diff --git a/sdks/python/src/agent_control/control_decorators.py b/sdks/python/src/agent_control/control_decorators.py index 931d1c88..3370be1d 100644 --- a/sdks/python/src/agent_control/control_decorators.py +++ b/sdks/python/src/agent_control/control_decorators.py @@ -1,12 +1,12 @@ """ Control decorator for server-side protection of agent functions. -This module provides a decorator that applies server-defined policies to agent functions. -Policies contain multiple controls (regex, list, Luna2, etc.) that are managed server-side. +This module provides a decorator that applies server-defined controls to agent functions. +Controls can be associated via policies and direct agent-control links. Architecture: SERVER defines: Policies -> Controls (stage, selector, evaluator, action) - SDK decorator: just marks WHERE the policy applies + SDK decorator: just marks WHERE controls are evaluated Usage: import agent_control @@ -15,12 +15,12 @@ agent_name="my-agent-identity", ) - # Apply the agent's assigned policy + # Apply controls associated with the agent @agent_control.control() async def chat(message: str) -> str: return await assistant.respond(message) - # The server's policy contains controls that define: + # Server-side controls define: # - stage: "pre" or "post" # - selector.path: "input" or "output" # - evaluator: regex, list, Luna2 evaluator, etc. @@ -726,15 +726,14 @@ async def _execute_with_control( def control(policy: str | None = None, step_name: str | None = None) -> Callable[[F], F]: """ - Decorator to apply server-defined policy at this code location. + Decorator to apply server-defined controls at this code location. - The policy's controls (stage, selector, evaluator, action) are defined - on the SERVER. This decorator just marks WHERE to apply the policy. + Controls (stage, selector, evaluator, action) are defined on the SERVER. + This decorator marks WHERE to evaluate controls for the current agent. Args: - policy: Optional policy name for documentation. The agent's assigned - policy is automatically used. This parameter is for clarity - in code when multiple policies exist. + policy: Optional policy name for documentation. This parameter is for + clarity in code when multiple policies exist. step_name: Optional custom name for this step. If not provided, uses the function name. @@ -746,20 +745,20 @@ def control(policy: str | None = None, step_name: str | None = None) -> Callable How it works: 1. Before function execution: Calls server with stage="pre" - - Server evaluates all "pre" controls in the agent's policy + - Server evaluates all matching "pre" controls for the agent 2. Function executes 3. After function execution: Calls server with stage="post" - - Server evaluates all "post" controls in the agent's policy + - Server evaluates all matching "post" controls for the agent Example: import agent_control - # Initialize agent (connects to server, loads policy) + # Initialize agent (connects to server, loads control associations) agent_control.init( agent_name="my-bot-identity", ) - # Apply the agent's policy (all controls) + # Apply controls associated with the agent @agent_control.control() async def chat(message: str) -> str: return await assistant.respond(message) From e5f54110db6fa6542d9400020fe3521ff28f2593 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 16:39:13 -0800 Subject: [PATCH 26/32] fix!: remove AgentSummary policy_id and align direct-control docs/tests --- models/src/agent_control_models/server.py | 4 - sdks/python/README.md | 2 +- .../src/generated/models/agent-summary.ts | 6 - server/README.md | 9 +- .../agent_control_server/endpoints/agents.py | 2 - server/tests/test_init_agent.py | 10 +- server/tests/test_policy_integration.py | 110 + ui/src/core/api/generated/api-types.ts | 8867 +++++++++-------- ui/src/core/api/types.ts | 4 +- .../controls/use-delete-control-flow.tsx | 4 +- ui/tests/agent-stats.spec.ts | 8 +- ui/tests/control-store.spec.ts | 13 +- ui/tests/fixtures.ts | 3 - ui/tests/home.spec.ts | 8 +- ui/tests/search-input.spec.ts | 10 +- 15 files changed, 4589 insertions(+), 4471 deletions(-) diff --git a/models/src/agent_control_models/server.py b/models/src/agent_control_models/server.py index 3ac0467d..f32c9f44 100644 --- a/models/src/agent_control_models/server.py +++ b/models/src/agent_control_models/server.py @@ -335,10 +335,6 @@ class AgentSummary(BaseModel): """Summary of an agent for list responses.""" agent_name: str = Field(..., description="Unique identifier of the agent") - policy_id: int | None = Field( - default=None, - description="Deprecated: first associated policy ID, if any", - ) policy_ids: list[int] = Field( default_factory=list, description="IDs of policies associated with the agent" ) diff --git a/sdks/python/README.md b/sdks/python/README.md index 804394c8..ced0c4df 100644 --- a/sdks/python/README.md +++ b/sdks/python/README.md @@ -205,7 +205,7 @@ def control(policy: Optional[str] = None): Decorator to protect a function with server-defined controls. **Parameters:** -- `policy`: Optional policy name to use (defaults to agent's assigned policy) +- `policy`: Optional policy label for code readability when multiple policies exist **Example:** ```python diff --git a/sdks/typescript/src/generated/models/agent-summary.ts b/sdks/typescript/src/generated/models/agent-summary.ts index 7e7a5472..8f9732c4 100644 --- a/sdks/typescript/src/generated/models/agent-summary.ts +++ b/sdks/typescript/src/generated/models/agent-summary.ts @@ -29,10 +29,6 @@ export type AgentSummary = { * Number of evaluators registered with the agent */ evaluatorCount: number; - /** - * Deprecated: first associated policy ID, if any - */ - policyId?: number | null | undefined; /** * IDs of policies associated with the agent */ @@ -51,7 +47,6 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = agent_name: types.string(), created_at: z.optional(z.nullable(types.string())), evaluator_count: z._default(types.number(), 0), - policy_id: z.optional(z.nullable(types.number())), policy_ids: types.optional(z.array(types.number())), step_count: z._default(types.number(), 0), }), @@ -61,7 +56,6 @@ export const AgentSummary$inboundSchema: z.ZodMiniType = "agent_name": "agentName", "created_at": "createdAt", "evaluator_count": "evaluatorCount", - "policy_id": "policyId", "policy_ids": "policyIds", "step_count": "stepCount", }); diff --git a/server/README.md b/server/README.md index 39435561..6abcbb71 100644 --- a/server/README.md +++ b/server/README.md @@ -163,7 +163,7 @@ Body: { "agent": {...}, "tools": [...], "force_replace": false } # Get agent GET /api/v1/agents/{agent_name} -# List controls for agent (based on assigned policy) +# List controls for agent (union of policy-associated + directly associated controls) GET /api/v1/agents/{agent_name}/controls ``` @@ -198,8 +198,11 @@ Body: { "name": "my-policy", "description": "..." } # List policies GET /api/v1/policies -# Assign policy to agent -POST /api/v1/policies/{policy_id}/agents/{agent_name} +# Associate policy with agent (additive; policy is optional) +POST /api/v1/agents/{agent_name}/policies/{policy_id} + +# Associate control directly with agent +POST /api/v1/agents/{agent_name}/controls/{control_id} # Add control to policy POST /api/v1/policies/{policy_id}/controls/{control_id} diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index 390a9967..bf031e55 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -378,12 +378,10 @@ async def list_agents( active_controls = control_counts_map.get(agent.name, 0) policy_ids = policy_ids_map.get(agent.name, []) - primary_policy_id = policy_ids[0] if policy_ids else None summaries.append( AgentSummary( agent_name=agent.name, - policy_id=primary_policy_id, policy_ids=policy_ids, created_at=agent.created_at.isoformat() if agent.created_at else None, step_count=step_count, diff --git a/server/tests/test_init_agent.py b/server/tests/test_init_agent.py index e67a0988..7140e736 100644 --- a/server/tests/test_init_agent.py +++ b/server/tests/test_init_agent.py @@ -541,18 +541,18 @@ def test_list_agents_returns_created_agents(client: TestClient) -> None: 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 agent1["policy_ids"] == [] 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 + assert agent2["policy_ids"] == [] def test_list_agents_with_policy(client: TestClient) -> None: - """Test that list agents shows policy_id when assigned.""" + """Test that list agents shows policy_ids when assigned.""" # Given: an agent with a policy assigned payload = make_agent_payload() client.post("/api/v1/agents/initAgent", json=payload) @@ -563,11 +563,11 @@ def test_list_agents_with_policy(client: TestClient) -> None: # When: listing agents resp = client.get("/api/v1/agents") - # Then: the agent shows the policy_id + # Then: the agent shows policy_ids assert resp.status_code == 200 body = resp.json() assert len(body["agents"]) == 1 - assert body["agents"][0]["policy_id"] == policy_id + assert body["agents"][0]["policy_ids"] == [policy_id] def test_list_agents_pagination(client: TestClient) -> None: diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index 129c2528..b556436f 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -292,3 +292,113 @@ def test_control_shared_between_policies(client: TestClient) -> None: assert control_id in {c["id"] for c in resp_a.json()["controls"]} assert control_id in {c["id"] for c in resp_b.json()["controls"]} + + +def test_agent_gets_controls_from_direct_associations(client: TestClient) -> None: + """Agent should see controls directly associated with it.""" + agent_name, _ = _create_agent(client) + control_1_id = _create_control(client) + control_2_id = _create_control(client) + + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_1_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_2_id}") + assert resp.status_code == 200 + + resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert resp.status_code == 200 + controls = resp.json()["controls"] + assert {control["id"] for control in controls} == {control_1_id, control_2_id} + + +def test_agent_controls_are_union_of_policy_and_direct_with_dedupe(client: TestClient) -> None: + """Agent control list should union policy + direct controls and de-duplicate by control id.""" + agent_name, _ = _create_agent(client) + policy_id = _create_policy(client) + + shared_control_id = _create_control(client) + policy_only_control_id = _create_control(client) + direct_only_control_id = _create_control(client) + + # Associate shared + policy-only controls via policy. + resp = client.post(f"/api/v1/policies/{policy_id}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/policies/{policy_id}/controls/{policy_only_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_id}") + assert resp.status_code == 200 + + # Associate shared + direct-only controls directly with agent. + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{direct_only_control_id}") + assert resp.status_code == 200 + + # Shared control should appear only once in active controls. + resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert resp.status_code == 200 + controls = resp.json()["controls"] + received_control_ids = {control["id"] for control in controls} + assert received_control_ids == {shared_control_id, policy_only_control_id, direct_only_control_id} + assert len(controls) == 3 + + # list_agents active_controls_count should reflect deduplicated union as well. + agents_resp = client.get("/api/v1/agents", params={"name": agent_name}) + assert agents_resp.status_code == 200 + matching_agents = [ + agent for agent in agents_resp.json()["agents"] if agent["agent_name"] == agent_name + ] + assert len(matching_agents) == 1 + assert matching_agents[0]["active_controls_count"] == 3 + + +def test_remove_direct_control_keeps_policy_inherited_control_active(client: TestClient) -> None: + """Removing a direct association should keep control active when policy still provides it.""" + agent_name, _ = _create_agent(client) + policy_id = _create_policy(client) + control_id = _create_control(client) + + resp = client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + + resp = client.delete(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["success"] is True + assert body["removed_direct_association"] is True + assert body["control_still_active"] is True + + # Idempotent behavior when no direct link remains but policy inheritance still exists. + resp = client.delete(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["removed_direct_association"] is False + assert body["control_still_active"] is True + + resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert resp.status_code == 200 + assert control_id in {control["id"] for control in resp.json()["controls"]} + + +def test_remove_direct_control_deactivates_when_not_inherited(client: TestClient) -> None: + """Removing a direct-only control should make it inactive for the agent.""" + agent_name, _ = _create_agent(client) + control_id = _create_control(client) + + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + + resp = client.delete(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["success"] is True + assert body["removed_direct_association"] is True + assert body["control_still_active"] is False + + resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert resp.status_code == 200 + assert control_id not in {control["id"] for control in resp.json()["controls"]} diff --git a/ui/src/core/api/generated/api-types.ts b/ui/src/core/api/generated/api-types.ts index a624d2e6..d54bbae1 100644 --- a/ui/src/core/api/generated/api-types.ts +++ b/ui/src/core/api/generated/api-types.ts @@ -4,4440 +4,4453 @@ */ export interface paths { - "/api/v1/agents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all agents - * @description List all registered agents with cursor-based pagination. - * - * Returns a summary of each agent including identifier, policy associations, - * and counts of registered steps and evaluators. - * - * Args: - * 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) - * - * Returns: - * ListAgentsResponse with agent summaries and pagination info - */ - get: operations["list_agents_api_v1_agents_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/initAgent": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Initialize or update an agent - * @description Register a new agent or update an existing agent's steps and metadata. - * - * This endpoint is idempotent: - * - If the agent name doesn't exist, creates a new agent - * - If the agent name exists, updates registration data in place - * - * 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 - * db: Database session (injected) - * - * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) - */ - post: operations["init_agent_api_v1_agents_initAgent_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent details - * @description Retrieve agent metadata and all registered steps. - * - * Returns the latest version of each step (deduplicated by type+name). - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * GetAgentResponse with agent metadata and step list - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 422: Agent data is corrupted - */ - get: operations["get_agent_api_v1_agents__agent_name__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Modify agent (remove steps/evaluators) - * @description Remove steps and/or evaluators from an agent. - * - * This is the complement to initAgent which only adds items. - * Removals are idempotent - attempting to remove non-existent items is not an error. - * - * Args: - * agent_name: Agent identifier - * request: Lists of step/evaluator identifiers to remove - * db: Database session (injected) - * - * Returns: - * PatchAgentResponse with lists of actually removed items - * - * Raises: - * HTTPException 404: Agent not found - * HTTPException 500: Database error during update - */ - patch: operations["patch_agent_api_v1_agents__agent_name__patch"]; - trace?: never; - }; - "/api/v1/agents/{agent_name}/controls": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's active controls - * @description List all protection controls active for an agent. - * - * Controls include the union of policy-derived and directly associated controls. - * - * Args: - * agent_name: Agent identifier - * db: Database session (injected) - * - * Returns: - * AgentControlsResponse with list of active controls - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations["list_agent_controls_api_v1_agents__agent_name__controls_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/controls/{control_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate control directly with agent - * @description Associate a control directly with an agent (idempotent). - */ - post: operations["add_agent_control_api_v1_agents__agent_name__controls__control_id__post"]; - /** - * Remove direct control association from agent - * @description Remove a direct control association from an agent (idempotent). - */ - delete: operations["remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/evaluators": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List agent's registered evaluator schemas - * @description List all evaluator schemas registered with an agent. - * - * Evaluator schemas are registered via initAgent and used for: - * - Config validation when creating Controls - * - UI to display available config options - * - * Args: - * 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) - * - * Returns: - * ListEvaluatorsResponse with evaluator schemas and pagination - * - * Raises: - * HTTPException 404: Agent not found - */ - get: operations["list_agent_evaluators_api_v1_agents__agent_name__evaluators_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/evaluators/{evaluator_name}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get specific evaluator schema - * @description Get a specific evaluator schema registered with an agent. - * - * Args: - * agent_name: Agent identifier - * evaluator_name: Name of the evaluator - * db: Database session (injected) - * - * Returns: - * EvaluatorSchemaItem with schema details - * - * Raises: - * HTTPException 404: Agent or evaluator not found - */ - get: operations["get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/policies": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List policies associated with agent - * @description List policy IDs associated with an agent. - */ - get: operations["get_agent_policies_api_v1_agents__agent_name__policies_get"]; - put?: never; - post?: never; - /** - * Remove all policy associations from agent - * @description Remove all policy associations from an agent. - */ - delete: operations["remove_all_agent_policies_api_v1_agents__agent_name__policies_delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/policies/{policy_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Associate policy with agent - * @description Associate a policy with an agent (idempotent). - */ - post: operations["add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post"]; - /** - * Remove policy association from agent - * @description Remove a policy association from an agent. - * - * Idempotent for existing resources: removing a non-associated link is a no-op. - * Missing agent/policy resources still return 404. - */ - delete: operations["remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/policy": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get agent's assigned policy (compatibility) - * @description Compatibility endpoint that returns the first associated policy. - */ - get: operations["get_agent_policy_api_v1_agents__agent_name__policy_get"]; - put?: never; - post?: never; - /** - * Remove agent's policy assignment (compatibility) - * @description Compatibility endpoint that removes all policy associations. - */ - delete: operations["delete_agent_policy_api_v1_agents__agent_name__policy_delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/agents/{agent_name}/policy/{policy_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Assign policy to agent (compatibility) - * @description Compatibility endpoint that replaces all policy associations with one policy. - */ - post: operations["set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/controls": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all controls - * @description List all controls with optional filtering and cursor-based pagination. - * - * Controls are returned ordered by ID descending (newest first). - * - * Args: - * cursor: ID of the last control from the previous page (for pagination) - * limit: Maximum number of controls to return (default 20, max 100) - * name: Optional filter by name (partial, case-insensitive match) - * enabled: Optional filter by enabled status - * step_type: Optional filter by step type (built-ins: 'tool', 'llm') - * stage: Optional filter by stage ('pre' or 'post') - * execution: Optional filter by execution ('server' or 'sdk') - * tag: Optional filter by tag - * db: Database session (injected) - * - * Returns: - * ListControlsResponse with control summaries and pagination info - * - * Example: - * GET /controls?limit=10&enabled=true&step_type=tool - */ - get: operations["list_controls_api_v1_controls_get"]; - /** - * Create a new control - * @description Create a new control with a unique name and empty data. - * - * Controls define protection logic and can be added to policies. - * Use the PUT /{control_id}/data endpoint to set control configuration. - * - * Args: - * request: Control creation request with unique name - * db: Database session (injected) - * - * Returns: - * CreateControlResponse with the new control's ID - * - * Raises: - * HTTPException 409: Control with this name already exists - * HTTPException 500: Database error during creation - */ - put: operations["create_control_api_v1_controls_put"]; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control details - * @description Retrieve a control by ID including its name and configuration data. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlResponse with control id, name, and data - * - * Raises: - * HTTPException 404: Control not found - */ - get: operations["get_control_api_v1_controls__control_id__get"]; - put?: never; - post?: never; - /** - * Delete a control - * @description Delete a control by ID. - * - * By default, deletion fails if the control is associated with any policy or agent. - * Use force=true to automatically dissociate and delete. - * - * Args: - * control_id: ID of the control to delete - * force: If true, remove associations before deleting - * db: Database session (injected) - * - * Returns: - * DeleteControlResponse with success flag and dissociation details - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: Control is in use (and force=false) - * HTTPException 500: Database error during deletion - */ - delete: operations["delete_control_api_v1_controls__control_id__delete"]; - options?: never; - head?: never; - /** - * Update control metadata - * @description Update control metadata (name and/or enabled status). - * - * This endpoint allows partial updates: - * - To rename: provide 'name' field - * - To enable/disable: provide 'enabled' field (updates the control's data) - * - * Args: - * control_id: ID of the control to update - * request: Fields to update (name, enabled) - * db: Database session (injected) - * - * Returns: - * PatchControlResponse with current control state - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 409: New name conflicts with existing control - * HTTPException 422: Cannot update enabled status (control has no data configured) - * HTTPException 500: Database error during update - */ - patch: operations["patch_control_api_v1_controls__control_id__patch"]; - trace?: never; - }; - "/api/v1/controls/{control_id}/data": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get control configuration data - * @description Retrieve the configuration data for a control. - * - * Control data is a JSONB field that must follow the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * db: Database session (injected) - * - * Returns: - * GetControlDataResponse with validated ControlDefinition - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 422: Control data is corrupted - */ - get: operations["get_control_data_api_v1_controls__control_id__data_get"]; - /** - * Update control configuration data - * @description Update the configuration data for a control. - * - * This replaces the entire data payload. The data is validated against - * the ControlDefinition schema. - * - * Args: - * control_id: ID of the control - * request: New control data (replaces existing) - * db: Database session (injected) - * - * Returns: - * SetControlDataResponse with success flag - * - * Raises: - * HTTPException 404: Control not found - * HTTPException 500: Database error during update - */ - put: operations["set_control_data_api_v1_controls__control_id__data_put"]; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - header?: never; - path?: never; - cookie?: never; - }; - /** List evaluator configs */ - get: operations["list_evaluator_configs_api_v1_evaluator_configs_get"]; - put?: never; - /** Create evaluator config */ - post: operations["create_evaluator_config_api_v1_evaluator_configs_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/evaluator-configs/{config_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get evaluator config */ - get: operations["get_evaluator_config_api_v1_evaluator_configs__config_id__get"]; - /** Update evaluator config */ - put: operations["update_evaluator_config_api_v1_evaluator_configs__config_id__put"]; - post?: never; - /** Delete evaluator config */ - delete: operations["delete_evaluator_config_api_v1_evaluator_configs__config_id__delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/evaluators": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List available evaluators - * @description List all available evaluators. - * - * Returns metadata and JSON Schema for each built-in evaluator. - * - * Built-in evaluators: - * - **regex**: Regular expression pattern matching - * - **list**: List-based value matching with flexible logic - * - **json**: JSON validation with schema, types, constraints - * - **sql**: SQL query validation - * - * Custom evaluators are registered per-agent via initAgent. - * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. - */ - get: operations["get_evaluators_api_v1_evaluators_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ingest Events - * @description Ingest batched control execution events. - * - * Events are stored directly to the database with ~5-20ms latency. - * - * Args: - * request: Batch of events to ingest - * ingestor: Event ingestor (injected) - * - * Returns: - * BatchEventsResponse with counts of received/processed/dropped - */ - post: operations["ingest_events_api_v1_observability_events_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/events/query": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Query Events - * @description Query raw control execution events. - * - * Supports filtering by: - * - 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_name: Filter by agent - * - control_ids: Filter by controls - * - actions: Filter by actions (allow, deny, warn, log) - * - matched: Filter by matched status - * - check_stages: Filter by check stage (pre, post) - * - applies_to: Filter by call type (llm_call, tool_call) - * - start_time/end_time: Filter by time range - * - * Results are paginated with limit/offset. - * - * Args: - * request: Query parameters - * store: Event store (injected) - * - * Returns: - * EventQueryResponse with matching events and pagination info - */ - post: operations["query_events_api_v1_observability_events_query_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Stats - * @description Get agent-level aggregated statistics. - * - * Returns totals across all controls plus per-control breakdown. - * Use /stats/controls/{control_id} for single control stats. - * - * Args: - * 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) - * - * Returns: - * StatsResponse with agent-level totals and per-control breakdown - */ - get: operations["get_stats_api_v1_observability_stats_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/stats/controls/{control_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Control Stats - * @description Get statistics for a single control. - * - * Returns stats for the specified control with optional time-series. - * - * Args: - * control_id: Control ID 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) - * - * Returns: - * ControlStatsResponse with control stats and optional timeseries - */ - get: operations["get_control_stats_api_v1_observability_stats_controls__control_id__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/observability/status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Status - * @description Get observability system status. - * - * Returns basic health information. - */ - get: operations["get_status_api_v1_observability_status_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/policies": { - 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. - * - * 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; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; + '/api/v1/agents': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; + /** + * List all agents + * @description List all registered agents with cursor-based pagination. + * + * Returns a summary of each agent including identifier, policy associations, + * and counts of registered steps and evaluators. + * + * Args: + * 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) + * + * Returns: + * ListAgentsResponse with agent summaries and pagination info + */ + get: operations['list_agents_api_v1_agents_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/initAgent': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Initialize or update an agent + * @description Register a new agent or update an existing agent's steps and metadata. + * + * This endpoint is idempotent: + * - If the agent name doesn't exist, creates a new agent + * - If the agent name exists, updates registration data in place + * + * 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 + * db: Database session (injected) + * + * Returns: + * InitAgentResponse with created flag and active controls (if policy assigned) + */ + post: operations['init_agent_api_v1_agents_initAgent_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent details + * @description Retrieve agent metadata and all registered steps. + * + * Returns the latest version of each step (deduplicated by type+name). + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * GetAgentResponse with agent metadata and step list + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 422: Agent data is corrupted + */ + get: operations['get_agent_api_v1_agents__agent_name__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Modify agent (remove steps/evaluators) + * @description Remove steps and/or evaluators from an agent. + * + * This is the complement to initAgent which only adds items. + * Removals are idempotent - attempting to remove non-existent items is not an error. + * + * Args: + * agent_name: Agent identifier + * request: Lists of step/evaluator identifiers to remove + * db: Database session (injected) + * + * Returns: + * PatchAgentResponse with lists of actually removed items + * + * Raises: + * HTTPException 404: Agent not found + * HTTPException 500: Database error during update + */ + patch: operations['patch_agent_api_v1_agents__agent_name__patch']; + trace?: never; + }; + '/api/v1/agents/{agent_name}/controls': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's active controls + * @description List all protection controls active for an agent. + * + * Controls include the union of policy-derived and directly associated controls. + * + * Args: + * agent_name: Agent identifier + * db: Database session (injected) + * + * Returns: + * AgentControlsResponse with list of active controls + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations['list_agent_controls_api_v1_agents__agent_name__controls_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/controls/{control_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate control directly with agent + * @description Associate a control directly with an agent (idempotent). + */ + post: operations['add_agent_control_api_v1_agents__agent_name__controls__control_id__post']; + /** + * Remove direct control association from agent + * @description Remove a direct control association from an agent (idempotent). + */ + delete: operations['remove_agent_control_api_v1_agents__agent_name__controls__control_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/evaluators': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List agent's registered evaluator schemas + * @description List all evaluator schemas registered with an agent. + * + * Evaluator schemas are registered via initAgent and used for: + * - Config validation when creating Controls + * - UI to display available config options + * + * Args: + * 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) + * + * Returns: + * ListEvaluatorsResponse with evaluator schemas and pagination + * + * Raises: + * HTTPException 404: Agent not found + */ + get: operations['list_agent_evaluators_api_v1_agents__agent_name__evaluators_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/evaluators/{evaluator_name}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get specific evaluator schema + * @description Get a specific evaluator schema registered with an agent. + * + * Args: + * agent_name: Agent identifier + * evaluator_name: Name of the evaluator + * db: Database session (injected) + * + * Returns: + * EvaluatorSchemaItem with schema details + * + * Raises: + * HTTPException 404: Agent or evaluator not found + */ + get: operations['get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/policies': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List policies associated with agent + * @description List policy IDs associated with an agent. + */ + get: operations['get_agent_policies_api_v1_agents__agent_name__policies_get']; + put?: never; + post?: never; + /** + * Remove all policy associations from agent + * @description Remove all policy associations from an agent. + */ + delete: operations['remove_all_agent_policies_api_v1_agents__agent_name__policies_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/policies/{policy_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Associate policy with agent + * @description Associate a policy with an agent (idempotent). + */ + post: operations['add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post']; + /** + * Remove policy association from agent + * @description Remove a policy association from an agent. + * + * Idempotent for existing resources: removing a non-associated link is a no-op. + * Missing agent/policy resources still return 404. + */ + delete: operations['remove_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/policy': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get agent's assigned policy (compatibility) + * @description Compatibility endpoint that returns the first associated policy. + */ + get: operations['get_agent_policy_api_v1_agents__agent_name__policy_get']; + put?: never; + post?: never; + /** + * Remove agent's policy assignment (compatibility) + * @description Compatibility endpoint that removes all policy associations. + */ + delete: operations['delete_agent_policy_api_v1_agents__agent_name__policy_delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/agents/{agent_name}/policy/{policy_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Assign policy to agent (compatibility) + * @description Compatibility endpoint that replaces all policy associations with one policy. + */ + post: operations['set_agent_policy_api_v1_agents__agent_name__policy__policy_id__post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/controls': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all controls + * @description List all controls with optional filtering and cursor-based pagination. + * + * Controls are returned ordered by ID descending (newest first). + * + * Args: + * cursor: ID of the last control from the previous page (for pagination) + * limit: Maximum number of controls to return (default 20, max 100) + * name: Optional filter by name (partial, case-insensitive match) + * enabled: Optional filter by enabled status + * step_type: Optional filter by step type (built-ins: 'tool', 'llm') + * stage: Optional filter by stage ('pre' or 'post') + * execution: Optional filter by execution ('server' or 'sdk') + * tag: Optional filter by tag + * db: Database session (injected) + * + * Returns: + * ListControlsResponse with control summaries and pagination info + * + * Example: + * GET /controls?limit=10&enabled=true&step_type=tool + */ + get: operations['list_controls_api_v1_controls_get']; + /** + * Create a new control + * @description Create a new control with a unique name and empty data. + * + * Controls define protection logic and can be added to policies. + * Use the PUT /{control_id}/data endpoint to set control configuration. + * + * Args: + * request: Control creation request with unique name + * db: Database session (injected) + * + * Returns: + * CreateControlResponse with the new control's ID + * + * Raises: + * HTTPException 409: Control with this name already exists + * HTTPException 500: Database error during creation + */ + put: operations['create_control_api_v1_controls_put']; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control details + * @description Retrieve a control by ID including its name and configuration data. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlResponse with control id, name, and data + * + * Raises: + * HTTPException 404: Control not found + */ + get: operations['get_control_api_v1_controls__control_id__get']; + put?: never; + post?: never; + /** + * Delete a control + * @description Delete a control by ID. + * + * By default, deletion fails if the control is associated with any policy or agent. + * Use force=true to automatically dissociate and delete. + * + * Args: + * control_id: ID of the control to delete + * force: If true, remove associations before deleting + * db: Database session (injected) + * + * Returns: + * DeleteControlResponse with success flag and dissociation details + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: Control is in use (and force=false) + * HTTPException 500: Database error during deletion + */ + delete: operations['delete_control_api_v1_controls__control_id__delete']; + options?: never; + head?: never; + /** + * Update control metadata + * @description Update control metadata (name and/or enabled status). + * + * This endpoint allows partial updates: + * - To rename: provide 'name' field + * - To enable/disable: provide 'enabled' field (updates the control's data) + * + * Args: + * control_id: ID of the control to update + * request: Fields to update (name, enabled) + * db: Database session (injected) + * + * Returns: + * PatchControlResponse with current control state + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 409: New name conflicts with existing control + * HTTPException 422: Cannot update enabled status (control has no data configured) + * HTTPException 500: Database error during update + */ + patch: operations['patch_control_api_v1_controls__control_id__patch']; + trace?: never; + }; + '/api/v1/controls/{control_id}/data': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get control configuration data + * @description Retrieve the configuration data for a control. + * + * Control data is a JSONB field that must follow the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * db: Database session (injected) + * + * Returns: + * GetControlDataResponse with validated ControlDefinition + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 422: Control data is corrupted + */ + get: operations['get_control_data_api_v1_controls__control_id__data_get']; + /** + * Update control configuration data + * @description Update the configuration data for a control. + * + * This replaces the entire data payload. The data is validated against + * the ControlDefinition schema. + * + * Args: + * control_id: ID of the control + * request: New control data (replaces existing) + * db: Database session (injected) + * + * Returns: + * SetControlDataResponse with success flag + * + * Raises: + * HTTPException 404: Control not found + * HTTPException 500: Database error during update + */ + put: operations['set_control_data_api_v1_controls__control_id__data_put']; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + header?: never; + path?: never; + cookie?: never; + }; + /** List evaluator configs */ + get: operations['list_evaluator_configs_api_v1_evaluator_configs_get']; + put?: never; + /** Create evaluator config */ + post: operations['create_evaluator_config_api_v1_evaluator_configs_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/evaluator-configs/{config_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get evaluator config */ + get: operations['get_evaluator_config_api_v1_evaluator_configs__config_id__get']; + /** Update evaluator config */ + put: operations['update_evaluator_config_api_v1_evaluator_configs__config_id__put']; + post?: never; + /** Delete evaluator config */ + delete: operations['delete_evaluator_config_api_v1_evaluator_configs__config_id__delete']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/evaluators': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available evaluators + * @description List all available evaluators. + * + * Returns metadata and JSON Schema for each built-in evaluator. + * + * Built-in evaluators: + * - **regex**: Regular expression pattern matching + * - **list**: List-based value matching with flexible logic + * - **json**: JSON validation with schema, types, constraints + * - **sql**: SQL query validation + * + * Custom evaluators are registered per-agent via initAgent. + * Use GET /agents/{agent_name}/evaluators to list agent-specific schemas. + */ + get: operations['get_evaluators_api_v1_evaluators_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/events': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ingest Events + * @description Ingest batched control execution events. + * + * Events are stored directly to the database with ~5-20ms latency. + * + * Args: + * request: Batch of events to ingest + * ingestor: Event ingestor (injected) + * + * Returns: + * BatchEventsResponse with counts of received/processed/dropped + */ + post: operations['ingest_events_api_v1_observability_events_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/events/query': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Query Events + * @description Query raw control execution events. + * + * Supports filtering by: + * - 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_name: Filter by agent + * - control_ids: Filter by controls + * - actions: Filter by actions (allow, deny, warn, log) + * - matched: Filter by matched status + * - check_stages: Filter by check stage (pre, post) + * - applies_to: Filter by call type (llm_call, tool_call) + * - start_time/end_time: Filter by time range + * + * Results are paginated with limit/offset. + * + * Args: + * request: Query parameters + * store: Event store (injected) + * + * Returns: + * EventQueryResponse with matching events and pagination info + */ + post: operations['query_events_api_v1_observability_events_query_post']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Stats + * @description Get agent-level aggregated statistics. + * + * Returns totals across all controls plus per-control breakdown. + * Use /stats/controls/{control_id} for single control stats. + * + * Args: + * 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) + * + * Returns: + * StatsResponse with agent-level totals and per-control breakdown + */ + get: operations['get_stats_api_v1_observability_stats_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/stats/controls/{control_id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Control Stats + * @description Get statistics for a single control. + * + * Returns stats for the specified control with optional time-series. + * + * Args: + * control_id: Control ID 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) + * + * Returns: + * ControlStatsResponse with control stats and optional timeseries + */ + get: operations['get_control_stats_api_v1_observability_stats_controls__control_id__get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/observability/status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Status + * @description Get observability system status. + * + * Returns basic health information. + */ + get: operations['get_status_api_v1_observability_status_get']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/policies': { + 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. + * + * 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; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { - schemas: { - /** - * Agent - * @description Agent metadata for registration and tracking. - * - * An agent represents an AI system that can be protected and monitored. - * 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_metadata": { - * "environment": "production", - * "team": "support" - * }, - * "agent_name": "customer-service-bot", - * "agent_version": "1.0.0" - * } - */ - Agent: { - /** - * Agent Created At - * @description ISO 8601 timestamp when agent was created - */ - agent_created_at?: string | null; - /** - * Agent Description - * @description Optional description of the agent's purpose - */ - agent_description?: string | null; - /** - * Agent Metadata - * @description Free-form metadata dictionary for custom properties - */ - 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 - */ - agent_updated_at?: string | null; - /** - * Agent Version - * @description Semantic version string (e.g. '1.0.0') - */ - agent_version?: string | null; - }; - /** AgentControlsResponse */ - AgentControlsResponse: { - /** - * Controls - * @description List of active controls associated with the agent - */ - controls: components["schemas"]["Control"][]; - }; - /** - * AgentRef - * @description Reference to an agent (for listing which agents use a control). - */ - AgentRef: { - /** - * Agent Name - * @description Agent name - */ - agent_name: string; - }; - /** - * AgentSummary - * @description Summary of an agent for list responses. - */ - AgentSummary: { - /** - * Active Controls Count - * @description Number of active controls for this agent - * @default 0 - */ - active_controls_count: number; - /** - * Agent Name - * @description Unique identifier of the agent - */ - agent_name: string; - /** - * Created At - * @description ISO 8601 timestamp when agent was created - */ - created_at?: string | null; - /** - * Evaluator Count - * @description Number of evaluators registered with the agent - * @default 0 - */ - evaluator_count: number; - /** - * Policy Id - * @description Deprecated: first associated policy ID, if any - */ - policy_id?: number | null; - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - /** - * Step Count - * @description Number of steps registered with the agent - * @default 0 - */ - step_count: number; - }; - /** AssocResponse */ - AssocResponse: { - /** - * Success - * @description Whether the association change succeeded - */ - success: boolean; - }; - /** - * BatchEventsRequest - * @description Request model for batch event ingestion. - * - * SDKs batch events and send them to the server periodically. - * This reduces HTTP overhead significantly (100x reduction). - * - * Attributes: - * events: List of control execution events to ingest - * @example { - * "events": [ - * { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_id": 123, - * "control_name": "sql-injection-check", - * "matched": true, - * "span_id": "00f067aa0ba902b7", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - * ] - * } - */ - BatchEventsRequest: { - /** - * Events - * @description List of events to ingest - */ - events: components["schemas"]["ControlExecutionEvent"][]; - }; - /** - * BatchEventsResponse - * @description Response model for batch event ingestion. - * - * Attributes: - * received: Number of events received - * enqueued: Number of events successfully enqueued - * dropped: Number of events dropped (queue full) - * status: Overall status ('queued', 'partial', 'failed') - */ - BatchEventsResponse: { - /** - * Dropped - * @description Number of events dropped - */ - dropped: number; - /** - * Enqueued - * @description Number of events enqueued - */ - enqueued: number; - /** - * Received - * @description Number of events received - */ - received: number; - /** - * Status - * @description Overall ingestion status - * @enum {string} - */ - 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. - * - * Note: Only fully-configured controls (with valid ControlDefinition) - * are returned from API endpoints. Unconfigured controls are filtered out. - */ - Control: { - control: components["schemas"]["ControlDefinition-Output"]; - /** Id */ - id: number; - /** Name */ - name: string; - }; - /** - * ControlAction - * @description What to do when control matches. - */ - ControlAction: { - /** - * Decision - * @description Action to take when control is triggered - * @enum {string} - */ - decision: "allow" | "deny" | "steer" | "warn" | "log"; - /** @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. */ - steering_context?: components["schemas"]["SteeringContext"] | null; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - "ControlDefinition-Input": { - /** @description What action to take when control matches */ - action: components["schemas"]["ControlAction"]; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components["schemas"]["EvaluatorSpec"]; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: "server" | "sdk"; - /** @description Which steps and stages this control applies to */ - scope?: components["schemas"]["ControlScope"]; - /** @description What data to select from the payload */ - selector: components["schemas"]["ControlSelector"]; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlDefinition - * @description A control definition to evaluate agent interactions. - * - * This model contains only the logic and configuration. - * Identity fields (id, name) are managed by the database. - * @example { - * "action": { - * "decision": "deny" - * }, - * "description": "Block outputs containing US Social Security Numbers", - * "enabled": true, - * "evaluator": { - * "config": { - * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" - * }, - * "name": "regex" - * }, - * "execution": "server", - * "scope": { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * }, - * "selector": { - * "path": "output" - * }, - * "tags": [ - * "pii", - * "compliance" - * ] - * } - */ - "ControlDefinition-Output": { - /** @description What action to take when control matches */ - action: components["schemas"]["ControlAction"]; - /** - * Description - * @description Detailed description of the control - */ - description?: string | null; - /** - * Enabled - * @description Whether this control is active - * @default true - */ - enabled: boolean; - /** @description How to evaluate the selected data */ - evaluator: components["schemas"]["EvaluatorSpec"]; - /** - * Execution - * @description Where this control executes - * @enum {string} - */ - execution: "server" | "sdk"; - /** @description Which steps and stages this control applies to */ - scope?: components["schemas"]["ControlScope"]; - /** @description What data to select from the payload */ - selector: components["schemas"]["ControlSelector"]; - /** - * Tags - * @description Tags for categorization - */ - tags?: string[]; - }; - /** - * ControlExecutionEvent - * @description Represents a single control execution event. - * - * This is the core observability data model, capturing: - * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) - * - Context: agent, control, check stage, applies to - * - Result: action taken, whether matched, confidence score - * - Timing: when it happened, how long it took - * - Optional details: evaluator name, selector path, errors, metadata - * - * Attributes: - * 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_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) - * applies_to: "llm_call" or "tool_call" - * action: The action taken (allow, deny, warn, log) - * matched: Whether the control evaluator matched - * confidence: Confidence score from the evaluator (0.0-1.0) - * timestamp: When the control was executed (UTC) - * execution_duration_ms: How long the control evaluation took - * evaluator_name: Name of the evaluator used - * selector_path: The selector path used to extract data - * error_message: Error message if evaluation failed - * metadata: Additional metadata for extensibility - * @example { - * "action": "deny", - * "agent_name": "my-agent", - * "applies_to": "llm_call", - * "check_stage": "pre", - * "confidence": 0.95, - * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", - * "control_id": 123, - * "control_name": "sql-injection-check", - * "evaluator_name": "regex", - * "execution_duration_ms": 15.3, - * "matched": true, - * "selector_path": "input", - * "span_id": "00f067aa0ba902b7", - * "timestamp": "2025-01-09T10:30:00Z", - * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" - * } - */ - ControlExecutionEvent: { - /** - * Action - * @description Action taken by the control - * @enum {string} - */ - action: "allow" | "deny" | "steer" | "warn" | "log"; - /** - * Agent Name - * @description Identifier of the agent - */ - agent_name: string; - /** - * Applies To - * @description Type of call: 'llm_call' or 'tool_call' - * @enum {string} - */ - applies_to: "llm_call" | "tool_call"; - /** - * Check Stage - * @description Check stage: 'pre' or 'post' - * @enum {string} - */ - check_stage: "pre" | "post"; - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Control Execution Id - * @description Unique ID for this control execution - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control (denormalized) - */ - control_name: string; - /** - * Error Message - * @description Error message if evaluation failed - */ - error_message?: string | null; - /** - * Evaluator Name - * @description Name of the evaluator used - */ - evaluator_name?: string | null; - /** - * Execution Duration Ms - * @description Execution duration in milliseconds - */ - execution_duration_ms?: number | null; - /** - * Matched - * @description Whether the evaluator matched (True) or not (False) - */ - matched: boolean; - /** - * Metadata - * @description Additional metadata - */ - metadata?: { - [key: string]: unknown; - }; - /** - * Selector Path - * @description Selector path used to extract data - */ - selector_path?: string | null; - /** - * Span Id - * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) - */ - span_id: string; - /** - * Timestamp - * Format: date-time - * @description When the control was executed (UTC) - */ - 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" | "steer" | "warn" | "log"; - /** - * Control Execution Id - * @description Unique ID for this control execution (generated by engine) - */ - control_execution_id?: string; - /** - * Control Id - * @description Database ID of the control - */ - control_id: number; - /** - * Control Name - * @description Name of the control - */ - control_name: string; - /** @description Evaluator result (confidence, message, metadata) */ - result: components["schemas"]["EvaluatorResult"]; - /** @description Steering context for steer actions if configured */ - steering_context?: components["schemas"]["SteeringContext"] | null; - }; - /** - * ControlScope - * @description Defines when a control applies to a Step. - * @example { - * "stages": [ - * "pre" - * ], - * "step_types": [ - * "tool" - * ] - * } - * @example { - * "step_names": [ - * "search_db", - * "fetch_user" - * ] - * } - * @example { - * "step_name_regex": "^db_.*" - * } - * @example { - * "stages": [ - * "post" - * ], - * "step_types": [ - * "llm" - * ] - * } - */ - ControlScope: { - /** - * Stages - * @description Evaluation stages this control applies to - */ - stages?: ("pre" | "post")[] | null; - /** - * Step Name Regex - * @description RE2 pattern matched with search() against step name - */ - step_name_regex?: string | null; - /** - * Step Names - * @description Exact step names this control applies to - */ - 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 - * @description Selects data from a Step payload. - * - * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" - * meaning the entire Step object. - * @example { - * "path": "output" - * } - * @example { - * "path": "context.user_id" - * } - * @example { - * "path": "input" - * } - * @example { - * "path": "*" - * } - * @example { - * "path": "name" - * } - * @example { - * "path": "output" - * } - */ - ControlSelector: { - /** - * Path - * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' - * @default * - */ - path: string | null; - }; - /** - * ControlStats - * @description Aggregated statistics for a single control. - * - * Attributes: - * control_id: Database ID of the control - * control_name: Name of the control - * execution_count: Total number of executions - * match_count: Number of times the control matched - * non_match_count: Number of times the control did not match - * allow_count: Number of allow actions - * deny_count: Number of deny actions - * steer_count: Number of steer actions - * warn_count: Number of warn actions - * log_count: Number of log actions - * error_count: Number of errors during evaluation - * avg_confidence: Average confidence score - * 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 - */ - control_id: number; - /** - * Control Name - * @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 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - */ - non_match_count: number; - /** - * Steer Count - * @description Steer actions - */ - steer_count: number; - /** - * Warn Count - * @description Warn actions - */ - warn_count: number; - }; - /** - * ControlStatsResponse - * @description Response model for control-level statistics. - * - * Contains stats for a single control (with optional timeseries). - * - * Attributes: - * agent_name: Agent identifier - * time_range: Time range used - * control_id: Control ID - * control_name: Control name - * stats: Control statistics (includes timeseries when requested) - */ - ControlStatsResponse: { - /** - * Agent Name - * @description Agent identifier - */ - agent_name: string; - /** - * Control Id - * @description Control ID - */ - control_id: number; - /** - * Control Name - * @description Control name - */ - 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: { - /** - * Description - * @description Control description - */ - description?: string | null; - /** - * Enabled - * @description Whether control is enabled - * @default true - */ - enabled: boolean; - /** - * Execution - * @description 'server' or 'sdk' - */ - execution?: string | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - /** - * Stages - * @description Evaluation stages in scope - */ - stages?: string[] | null; - /** - * Step Types - * @description Step types in scope - */ - step_types?: string[] | null; - /** - * Tags - * @description Control tags - */ - tags?: string[]; - /** @description Agent using this control */ - used_by_agent?: components["schemas"]["AgentRef"] | null; - /** - * Used By Agents Count - * @description Number of unique agents using this control - * @default 0 - */ - used_by_agents_count: number; - }; - /** CreateControlRequest */ - CreateControlRequest: { - /** - * Name - * @description Unique control name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreateControlResponse */ - CreateControlResponse: { - /** - * Control Id - * @description Identifier of the created control - */ - control_id: number; - }; - /** - * CreateEvaluatorConfigRequest - * @description Request to create an evaluator config template. - */ - CreateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyRequest */ - CreatePolicyRequest: { - /** - * Name - * @description Unique policy name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** CreatePolicyResponse */ - CreatePolicyResponse: { - /** - * Policy Id - * @description Identifier of the created policy - */ - policy_id: number; - }; - /** - * DeleteControlResponse - * @description Response for deleting a control. - */ - DeleteControlResponse: { - /** - * Dissociated From - * @description Deprecated: policy IDs the control was removed from before deletion - */ - dissociated_from?: number[]; - /** - * Dissociated From Agents - * @description Agent names the control was removed from before deletion - */ - dissociated_from_agents?: string[]; - /** - * Dissociated From Policies - * @description Policy IDs the control was removed from before deletion - */ - dissociated_from_policies?: number[]; - /** - * Success - * @description Whether the control was deleted - */ - success: boolean; - }; - /** - * DeleteEvaluatorConfigResponse - * @description Response for deleting an evaluator config. - */ - DeleteEvaluatorConfigResponse: { - /** - * Success - * @description Whether the evaluator config was deleted - */ - success: boolean; - }; - /** - * DeletePolicyResponse - * @description Compatibility response for singular policy deletion endpoint. - */ - DeletePolicyResponse: { - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * EvaluationRequest - * @description Request model for evaluation analysis. - * - * Used to analyze agent interactions for safety violations, - * policy compliance, and control rules. - * - * Attributes: - * 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_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "session_id": "abc123", - * "user_id": "user123" - * }, - * "input": "What is the customer's credit card number?", - * "name": "support-answer", - * "output": "I cannot share sensitive payment information.", - * "type": "llm" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "pre", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "type": "tool" - * } - * } - * @example { - * "agent_name": "customer-service-bot", - * "stage": "post", - * "step": { - * "context": { - * "user_id": "user123" - * }, - * "input": { - * "query": "SELECT * FROM users" - * }, - * "name": "search_database", - * "output": { - * "results": [] - * }, - * "type": "tool" - * } - * } - */ - EvaluationRequest: { - /** - * Agent Name - * @description Identifier of the agent making the evaluation request - */ - 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 - * @description Response model from evaluation analysis (server-side). - * - * This is what the server returns. The SDK may transform this - * into an EvaluationResult for client convenience. - * - * Attributes: - * is_safe: Whether the content is considered safe - * confidence: Confidence score between 0.0 and 1.0 - * reason: Optional explanation for the decision - * matches: List of controls that matched/triggered (if any) - * errors: List of controls that failed during evaluation (if any) - * non_matches: List of controls that were evaluated but did not match (if any) - */ - EvaluationResponse: { - /** - * Confidence - * @description Confidence score (0.0 to 1.0) - */ - confidence: number; - /** - * Errors - * @description List of controls that failed during evaluation (if any) - */ - 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; - /** - * Non Matches - * @description List of controls that were evaluated but did not match (if any) - */ - non_matches?: components["schemas"]["ControlMatch"][] | null; - /** - * Reason - * @description Explanation for the decision - */ - reason?: string | null; - }; - /** - * EvaluatorConfigItem - * @description Evaluator config template stored in the server. - */ - EvaluatorConfigItem: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Created At - * @description ISO 8601 created timestamp - */ - created_at?: string | null; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Id - * @description Evaluator config ID - */ - id: number; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - /** - * Updated At - * @description ISO 8601 updated timestamp - */ - updated_at?: string | null; - }; - /** - * EvaluatorInfo - * @description Information about a registered evaluator. - */ - EvaluatorInfo: { - /** - * Config Schema - * @description JSON Schema for config - */ - 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 - */ - requires_api_key: boolean; - /** - * Timeout Ms - * @description Default timeout in milliseconds - */ - timeout_ms: number; - /** - * Version - * @description Evaluator version - */ - version: string; - }; - /** - * EvaluatorResult - * @description Result from a control evaluator. - * - * The `error` field indicates evaluator failures, NOT validation failures: - * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors - * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures - * - * When `error` is set, `matched` must be False (fail-open on evaluator errors). - * When `error` is None, `matched` reflects the actual validation result. - * - * This distinction allows: - * - Clients to distinguish "data violated rules" from "evaluator is broken" - * - Observability systems to monitor evaluator health separately from validation outcomes - */ - EvaluatorResult: { - /** - * 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 - */ - message?: string | null; - /** - * Metadata - * @description Additional result metadata - */ - metadata?: { - [key: string]: unknown; - } | null; - }; - /** - * EvaluatorSchema - * @description Schema for a custom evaluator registered with an agent. - * - * Custom evaluators are Evaluator classes deployed with the engine. - * This schema is registered via initAgent for validation and UI purposes. - */ - EvaluatorSchema: { - /** - * Config Schema - * @description JSON Schema for evaluator config validation - */ - config_schema?: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Name - * @description Unique evaluator name - */ - name: string; - }; - /** - * EvaluatorSchemaItem - * @description Evaluator schema summary for list response. - */ - EvaluatorSchemaItem: { - /** 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 - * @description Request model for querying raw events. - * - * Supports filtering by various criteria and pagination. - * - * Attributes: - * 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_name: Filter by agent identifier - * control_ids: Filter by control IDs - * actions: Filter by actions (allow, deny, steer, 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_name": "my-agent", - * "limit": 50, - * "start_time": "2025-01-09T00:00:00Z" - * } - */ - EventQueryRequest: { - /** - * Actions - * @description Filter by actions - */ - actions?: ("allow" | "deny" | "steer" | "warn" | "log")[] | null; - /** - * Agent Name - * @description Filter by agent identifier - */ - 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; - /** - * Control Execution Id - * @description Filter by specific event ID - */ - control_execution_id?: string | null; - /** - * Control Ids - * @description Filter by control IDs - */ - control_ids?: number[] | null; - /** - * End Time - * @description Filter events before this time - */ - end_time?: string | null; - /** - * Limit - * @description Maximum events - * @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 - * @description Response model for event queries. - * - * Attributes: - * events: List of matching events - * total: Total number of matching events (for pagination) - * limit: Limit used in query - * offset: Offset used in query - */ - EventQueryResponse: { - /** - * Events - * @description Matching events - */ - events: components["schemas"]["ControlExecutionEvent"][]; - /** - * Limit - * @description Limit used in query - */ - limit: number; - /** - * Offset - * @description Offset used in query - */ - offset: number; - /** - * Total - * @description Total matching events - */ - total: number; - }; - /** GetAgentPoliciesResponse */ - GetAgentPoliciesResponse: { - /** - * Policy Ids - * @description IDs of policies associated with the agent - */ - policy_ids?: number[]; - }; - /** - * GetAgentResponse - * @description Response containing agent details and registered steps. - */ - GetAgentResponse: { - /** @description Agent metadata */ - agent: components["schemas"]["Agent"]; - /** - * 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: { - /** @description Control data payload */ - data: components["schemas"]["ControlDefinition-Output"]; - }; - /** - * GetControlResponse - * @description Response containing control details. - */ - GetControlResponse: { - /** @description Control configuration data (None if not yet configured) */ - data?: components["schemas"]["ControlDefinition-Output"] | null; - /** - * Id - * @description Control ID - */ - id: number; - /** - * Name - * @description Control name - */ - name: string; - }; - /** - * GetPolicyControlsResponse - * @description Response containing control IDs associated with a policy. - */ - GetPolicyControlsResponse: { - /** - * Control Ids - * @description List of control IDs associated with the policy - */ - control_ids: number[]; - }; - /** - * GetPolicyResponse - * @description Compatibility response for singular policy retrieval endpoint. - */ - GetPolicyResponse: { - /** - * Policy Id - * @description Associated policy ID - */ - policy_id: number; - }; - /** HTTPValidationError */ - HTTPValidationError: { - /** Detail */ - detail?: components["schemas"]["ValidationError"][]; - }; - /** - * HealthResponse - * @description Health check response model. - * - * Attributes: - * status: Current health status (e.g., "healthy", "degraded", "unhealthy") - * version: Application version - */ - HealthResponse: { - /** Status */ - status: string; - /** 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_name": "customer-service-bot", - * "agent_version": "1.0.0" - * }, - * "evaluators": [ - * { - * "config_schema": { - * "properties": { - * "sensitivity": { - * "type": "string" - * } - * }, - * "type": "object" - * }, - * "description": "Detects PII in text", - * "name": "pii-detector" - * } - * ], - * "steps": [ - * { - * "input_schema": { - * "query": { - * "type": "string" - * } - * }, - * "name": "search_kb", - * "output_schema": { - * "results": { - * "type": "array" - * } - * }, - * "type": "tool" - * } - * ] - * } - */ - InitAgentRequest: { - /** @description Agent metadata including ID, name, and version */ - agent: components["schemas"]["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 - */ - conflict_mode: components["schemas"]["ConflictMode"]; - /** - * Evaluators - * @description Custom evaluator schemas for config validation - */ - evaluators?: components["schemas"]["EvaluatorSchema"][]; - /** - * Force Replace - * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. - * @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 - */ - controls?: components["schemas"]["Control"][]; - /** - * Created - * @description True if agent was newly created, False if updated - */ - created: boolean; - /** - * Overwrite Applied - * @description True if overwrite mode changed registration data on an existing agent - * @default false - */ - overwrite_applied: boolean; - /** @description Detailed list of changes applied in overwrite mode */ - overwrite_changes?: components["schemas"]["InitAgentOverwriteChanges"]; - }; - JSONObject: { - [key: string]: components["schemas"]["JSONValue"]; - }; - /** @description Any JSON value */ - JSONValue: unknown; - /** - * ListAgentsResponse - * @description Response for listing agents. - */ - ListAgentsResponse: { - /** - * Agents - * @description List of agent summaries - */ - agents: components["schemas"]["AgentSummary"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListControlsResponse - * @description Response for listing controls. - */ - ListControlsResponse: { - /** - * Controls - * @description List of control summaries - */ - controls: components["schemas"]["ControlSummary"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListEvaluatorConfigsResponse - * @description Response for listing evaluator configs. - */ - ListEvaluatorConfigsResponse: { - /** - * Evaluator Configs - * @description List of evaluator configs - */ - evaluator_configs: components["schemas"]["EvaluatorConfigItem"][]; - /** @description Pagination metadata */ - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * ListEvaluatorsResponse - * @description Response for listing agent's evaluator schemas. - */ - ListEvaluatorsResponse: { - /** Evaluators */ - evaluators: components["schemas"]["EvaluatorSchemaItem"][]; - pagination: components["schemas"]["PaginationInfo"]; - }; - /** - * PaginationInfo - * @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; - /** - * Next Cursor - * @description Cursor for fetching the next page (null if no more pages) - */ - next_cursor?: string | null; - /** - * Total - * @description Total number of items - */ - total: number; - }; - /** - * PatchAgentRequest - * @description Request to modify an agent (remove steps/evaluators). - */ - PatchAgentRequest: { - /** - * 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: { - /** - * 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: { - /** - * Enabled - * @description Enable or disable the control - */ - enabled?: boolean | null; - /** - * Name - * @description New name for the control - */ - name?: string | null; - }; - /** - * PatchControlResponse - * @description Response from control metadata update. - */ - PatchControlResponse: { - /** - * Enabled - * @description Current enabled status (if control has data configured) - */ - enabled?: boolean | null; - /** - * Name - * @description Current control name (may have changed) - */ - name: string; - /** - * Success - * @description Whether the update succeeded - */ - success: boolean; - }; - /** - * RemoveAgentControlResponse - * @description Response for removing a direct agent-control association. - */ - RemoveAgentControlResponse: { - /** - * Control Still Active - * @description True if the control remains active via policy association(s) - */ - control_still_active: boolean; - /** - * Removed Direct Association - * @description True if a direct agent-control link was removed - */ - removed_direct_association: boolean; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * SetControlDataRequest - * @description Request to update control configuration data. - */ - SetControlDataRequest: { - /** @description Control configuration data (replaces existing) */ - data: components["schemas"]["ControlDefinition-Input"]; - }; - /** SetControlDataResponse */ - SetControlDataResponse: { - /** - * Success - * @description Whether the control data was updated - */ - success: boolean; - }; - /** - * SetPolicyResponse - * @description Compatibility response for singular policy assignment endpoint. - */ - SetPolicyResponse: { - /** - * Old Policy Id - * @description Previously associated policy ID, if any - */ - old_policy_id?: number | null; - /** - * Success - * @description Whether the request succeeded - */ - success: boolean; - }; - /** - * StatsResponse - * @description Response model for agent-level aggregated statistics. - * - * Contains agent-level totals (with optional timeseries) and per-control breakdown. - * - * Attributes: - * 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 Name - * @description Agent identifier - */ - agent_name: string; - /** - * Controls - * @description Per-control breakdown - */ - controls: components["schemas"]["ControlStats"][]; - /** - * Time Range - * @description Time range used - */ - time_range: string; - /** @description Agent-level aggregate statistics */ - totals: components["schemas"]["StatsTotals"]; - }; - /** - * StatsTotals - * @description Agent-level aggregate statistics. - * - * Invariant: execution_count = match_count + non_match_count + error_count - * - * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. - * sum(action_counts.values()) == match_count - * - * Attributes: - * execution_count: Total executions across all controls - * match_count: Total matches across all controls (evaluator matched) - * non_match_count: Total non-matches across all controls (evaluator didn't match) - * error_count: Total errors across all controls (evaluation failed) - * action_counts: Breakdown of actions for matched executions - * timeseries: Time-series data points (only when include_timeseries=true) - */ - StatsTotals: { - /** - * Action Counts - * @description Action breakdown for matches: {allow, deny, steer, warn, log} - */ - action_counts?: { - [key: string]: number; - }; - /** - * Error Count - * @description Total errors - * @default 0 - */ - error_count: number; - /** - * Execution Count - * @description Total executions - */ - execution_count: number; - /** - * Match Count - * @description Total matches - * @default 0 - */ - match_count: number; - /** - * Non Match Count - * @description Total non-matches - * @default 0 - */ - non_match_count: number; - /** - * Timeseries - * @description Time-series data points (only when include_timeseries=true) - */ - timeseries?: components["schemas"]["TimeseriesBucket"][] | null; - }; - /** - * SteeringContext - * @description Steering context for steer actions. - * - * This model provides an extensible structure for steering guidance. - * Future fields could include severity, categories, suggested_actions, etc. - * @example { - * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." - * } - * @example { - * "message": "Transfer exceeds daily limit. Steps: 1) Ask user for business justification, 2) Request manager approval with amount and justification, 3) If approved, retry with manager_approved=True and justification filled in." - * } - */ - SteeringContext: { - /** - * Message - * @description Guidance message explaining what needs to be corrected and how - */ - message: string; - }; - /** - * Step - * @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 Output content for this step (None for pre-checks) */ - output?: components["schemas"]["JSONValue"] | null; - /** - * Type - * @description Step type (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * StepKey - * @description Identifies a registered step schema by type and name. - */ - StepKey: { - /** - * Name - * @description Registered step name - */ - name: string; - /** - * Type - * @description Step type - */ - type: string; - }; - /** - * StepSchema - * @description Schema for a registered agent step. - * @example { - * "description": "Search the internal knowledge base", - * "input_schema": { - * "query": { - * "description": "Search query", - * "type": "string" - * } - * }, - * "name": "search_knowledge_base", - * "output_schema": { - * "results": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "type": "tool" - * } - * @example { - * "description": "Customer support response generation", - * "input_schema": { - * "messages": { - * "items": { - * "type": "object" - * }, - * "type": "array" - * } - * }, - * "name": "support-answer", - * "output_schema": { - * "text": { - * "type": "string" - * } - * }, - * "type": "llm" - * } - */ - StepSchema: { - /** - * Description - * @description Optional description of the step - */ - description?: string | null; - /** - * Input Schema - * @description JSON schema describing step input - */ - 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 - */ - output_schema?: { - [key: string]: unknown; - } | null; - /** - * Type - * @description Step type for this schema (e.g., 'tool', 'llm') - */ - type: string; - }; - /** - * TimeseriesBucket - * @description Single data point in a time-series. - * - * Represents aggregated metrics for a single time bucket. - * - * Attributes: - * timestamp: Start time of the bucket (UTC, always timezone-aware) - * execution_count: Total executions in this bucket - * match_count: Number of matches in this bucket - * non_match_count: Number of non-matches in this bucket - * error_count: Number of errors in this bucket - * action_counts: Breakdown of actions for matched executions - * avg_confidence: Average confidence score (None if no executions) - * avg_duration_ms: Average execution duration in milliseconds (None if no data) - */ - TimeseriesBucket: { - /** - * Action Counts - * @description Action breakdown: {allow, deny, steer, warn, log} - */ - 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 - */ - execution_count: number; - /** - * Match Count - * @description Matches in bucket - */ - match_count: number; - /** - * Non Match Count - * @description Non-matches in bucket - */ - non_match_count: number; - /** - * Timestamp - * Format: date-time - * @description Start time of the bucket (UTC) - */ - timestamp: string; - }; - /** - * UpdateEvaluatorConfigRequest - * @description Request to replace an evaluator config template. - */ - UpdateEvaluatorConfigRequest: { - /** - * Config - * @description Evaluator-specific configuration - */ - config: { - [key: string]: unknown; - }; - /** - * Description - * @description Optional description - */ - description?: string | null; - /** - * Evaluator - * @description Evaluator name (built-in or custom) - */ - evaluator: string; - /** - * Name - * @description Unique evaluator config name (letters, numbers, hyphens, underscores) - */ - name: string; - }; - /** - * ValidateControlDataRequest - * @description Request to validate control configuration data without saving. - */ - ValidateControlDataRequest: { - /** @description Control configuration data to validate */ - data: components["schemas"]["ControlDefinition-Input"]; - }; - /** 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 */ - msg: string; - /** Error Type */ - type: string; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: { + /** + * Agent + * @description Agent metadata for registration and tracking. + * + * An agent represents an AI system that can be protected and monitored. + * 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_metadata": { + * "environment": "production", + * "team": "support" + * }, + * "agent_name": "customer-service-bot", + * "agent_version": "1.0.0" + * } + */ + Agent: { + /** + * Agent Created At + * @description ISO 8601 timestamp when agent was created + */ + agent_created_at?: string | null; + /** + * Agent Description + * @description Optional description of the agent's purpose + */ + agent_description?: string | null; + /** + * Agent Metadata + * @description Free-form metadata dictionary for custom properties + */ + 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 + */ + agent_updated_at?: string | null; + /** + * Agent Version + * @description Semantic version string (e.g. '1.0.0') + */ + agent_version?: string | null; + }; + /** AgentControlsResponse */ + AgentControlsResponse: { + /** + * Controls + * @description List of active controls associated with the agent + */ + controls: components['schemas']['Control'][]; + }; + /** + * AgentRef + * @description Reference to an agent (for listing which agents use a control). + */ + AgentRef: { + /** + * Agent Name + * @description Agent name + */ + agent_name: string; + }; + /** + * AgentSummary + * @description Summary of an agent for list responses. + */ + AgentSummary: { + /** + * Active Controls Count + * @description Number of active controls for this agent + * @default 0 + */ + active_controls_count: number; + /** + * Agent Name + * @description Unique identifier of the agent + */ + agent_name: string; + /** + * Created At + * @description ISO 8601 timestamp when agent was created + */ + created_at?: string | null; + /** + * Evaluator Count + * @description Number of evaluators registered with the agent + * @default 0 + */ + evaluator_count: number; + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + /** + * Step Count + * @description Number of steps registered with the agent + * @default 0 + */ + step_count: number; + }; + /** AssocResponse */ + AssocResponse: { + /** + * Success + * @description Whether the association change succeeded + */ + success: boolean; + }; + /** + * BatchEventsRequest + * @description Request model for batch event ingestion. + * + * SDKs batch events and send them to the server periodically. + * This reduces HTTP overhead significantly (100x reduction). + * + * Attributes: + * events: List of control execution events to ingest + * @example { + * "events": [ + * { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_id": 123, + * "control_name": "sql-injection-check", + * "matched": true, + * "span_id": "00f067aa0ba902b7", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + * ] + * } + */ + BatchEventsRequest: { + /** + * Events + * @description List of events to ingest + */ + events: components['schemas']['ControlExecutionEvent'][]; + }; + /** + * BatchEventsResponse + * @description Response model for batch event ingestion. + * + * Attributes: + * received: Number of events received + * enqueued: Number of events successfully enqueued + * dropped: Number of events dropped (queue full) + * status: Overall status ('queued', 'partial', 'failed') + */ + BatchEventsResponse: { + /** + * Dropped + * @description Number of events dropped + */ + dropped: number; + /** + * Enqueued + * @description Number of events enqueued + */ + enqueued: number; + /** + * Received + * @description Number of events received + */ + received: number; + /** + * Status + * @description Overall ingestion status + * @enum {string} + */ + 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. + * + * Note: Only fully-configured controls (with valid ControlDefinition) + * are returned from API endpoints. Unconfigured controls are filtered out. + */ + Control: { + control: components['schemas']['ControlDefinition-Output']; + /** Id */ + id: number; + /** Name */ + name: string; + }; + /** + * ControlAction + * @description What to do when control matches. + */ + ControlAction: { + /** + * Decision + * @description Action to take when control is triggered + * @enum {string} + */ + decision: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; + /** @description Steering context object for steer actions. Strongly recommended when decision='steer' to provide correction suggestions. If not provided, the evaluator result message will be used as fallback. */ + steering_context?: components['schemas']['SteeringContext'] | null; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + 'ControlDefinition-Input': { + /** @description What action to take when control matches */ + action: components['schemas']['ControlAction']; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components['schemas']['EvaluatorSpec']; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: 'server' | 'sdk'; + /** @description Which steps and stages this control applies to */ + scope?: components['schemas']['ControlScope']; + /** @description What data to select from the payload */ + selector: components['schemas']['ControlSelector']; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlDefinition + * @description A control definition to evaluate agent interactions. + * + * This model contains only the logic and configuration. + * Identity fields (id, name) are managed by the database. + * @example { + * "action": { + * "decision": "deny" + * }, + * "description": "Block outputs containing US Social Security Numbers", + * "enabled": true, + * "evaluator": { + * "config": { + * "pattern": "\\b\\d{3}-\\d{2}-\\d{4}\\b" + * }, + * "name": "regex" + * }, + * "execution": "server", + * "scope": { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * }, + * "selector": { + * "path": "output" + * }, + * "tags": [ + * "pii", + * "compliance" + * ] + * } + */ + 'ControlDefinition-Output': { + /** @description What action to take when control matches */ + action: components['schemas']['ControlAction']; + /** + * Description + * @description Detailed description of the control + */ + description?: string | null; + /** + * Enabled + * @description Whether this control is active + * @default true + */ + enabled: boolean; + /** @description How to evaluate the selected data */ + evaluator: components['schemas']['EvaluatorSpec']; + /** + * Execution + * @description Where this control executes + * @enum {string} + */ + execution: 'server' | 'sdk'; + /** @description Which steps and stages this control applies to */ + scope?: components['schemas']['ControlScope']; + /** @description What data to select from the payload */ + selector: components['schemas']['ControlSelector']; + /** + * Tags + * @description Tags for categorization + */ + tags?: string[]; + }; + /** + * ControlExecutionEvent + * @description Represents a single control execution event. + * + * This is the core observability data model, capturing: + * - Identity: control_execution_id, trace_id, span_id (OpenTelemetry-compatible) + * - Context: agent, control, check stage, applies to + * - Result: action taken, whether matched, confidence score + * - Timing: when it happened, how long it took + * - Optional details: evaluator name, selector path, errors, metadata + * + * Attributes: + * 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_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) + * applies_to: "llm_call" or "tool_call" + * action: The action taken (allow, deny, warn, log) + * matched: Whether the control evaluator matched + * confidence: Confidence score from the evaluator (0.0-1.0) + * timestamp: When the control was executed (UTC) + * execution_duration_ms: How long the control evaluation took + * evaluator_name: Name of the evaluator used + * selector_path: The selector path used to extract data + * error_message: Error message if evaluation failed + * metadata: Additional metadata for extensibility + * @example { + * "action": "deny", + * "agent_name": "my-agent", + * "applies_to": "llm_call", + * "check_stage": "pre", + * "confidence": 0.95, + * "control_execution_id": "550e8400-e29b-41d4-a716-446655440000", + * "control_id": 123, + * "control_name": "sql-injection-check", + * "evaluator_name": "regex", + * "execution_duration_ms": 15.3, + * "matched": true, + * "selector_path": "input", + * "span_id": "00f067aa0ba902b7", + * "timestamp": "2025-01-09T10:30:00Z", + * "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" + * } + */ + ControlExecutionEvent: { + /** + * Action + * @description Action taken by the control + * @enum {string} + */ + action: 'allow' | 'deny' | 'steer' | 'warn' | 'log'; + /** + * Agent Name + * @description Identifier of the agent + */ + agent_name: string; + /** + * Applies To + * @description Type of call: 'llm_call' or 'tool_call' + * @enum {string} + */ + applies_to: 'llm_call' | 'tool_call'; + /** + * Check Stage + * @description Check stage: 'pre' or 'post' + * @enum {string} + */ + check_stage: 'pre' | 'post'; + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Control Execution Id + * @description Unique ID for this control execution + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control (denormalized) + */ + control_name: string; + /** + * Error Message + * @description Error message if evaluation failed + */ + error_message?: string | null; + /** + * Evaluator Name + * @description Name of the evaluator used + */ + evaluator_name?: string | null; + /** + * Execution Duration Ms + * @description Execution duration in milliseconds + */ + execution_duration_ms?: number | null; + /** + * Matched + * @description Whether the evaluator matched (True) or not (False) + */ + matched: boolean; + /** + * Metadata + * @description Additional metadata + */ + metadata?: { + [key: string]: unknown; + }; + /** + * Selector Path + * @description Selector path used to extract data + */ + selector_path?: string | null; + /** + * Span Id + * @description Span ID for distributed tracing (SDK generates OTEL-compatible 16-char hex) + */ + span_id: string; + /** + * Timestamp + * Format: date-time + * @description When the control was executed (UTC) + */ + 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' | 'steer' | 'warn' | 'log'; + /** + * Control Execution Id + * @description Unique ID for this control execution (generated by engine) + */ + control_execution_id?: string; + /** + * Control Id + * @description Database ID of the control + */ + control_id: number; + /** + * Control Name + * @description Name of the control + */ + control_name: string; + /** @description Evaluator result (confidence, message, metadata) */ + result: components['schemas']['EvaluatorResult']; + /** @description Steering context for steer actions if configured */ + steering_context?: components['schemas']['SteeringContext'] | null; + }; + /** + * ControlScope + * @description Defines when a control applies to a Step. + * @example { + * "stages": [ + * "pre" + * ], + * "step_types": [ + * "tool" + * ] + * } + * @example { + * "step_names": [ + * "search_db", + * "fetch_user" + * ] + * } + * @example { + * "step_name_regex": "^db_.*" + * } + * @example { + * "stages": [ + * "post" + * ], + * "step_types": [ + * "llm" + * ] + * } + */ + ControlScope: { + /** + * Stages + * @description Evaluation stages this control applies to + */ + stages?: ('pre' | 'post')[] | null; + /** + * Step Name Regex + * @description RE2 pattern matched with search() against step name + */ + step_name_regex?: string | null; + /** + * Step Names + * @description Exact step names this control applies to + */ + 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 + * @description Selects data from a Step payload. + * + * - path: which slice of the Step to feed into the evaluator. Optional, defaults to "*" + * meaning the entire Step object. + * @example { + * "path": "output" + * } + * @example { + * "path": "context.user_id" + * } + * @example { + * "path": "input" + * } + * @example { + * "path": "*" + * } + * @example { + * "path": "name" + * } + * @example { + * "path": "output" + * } + */ + ControlSelector: { + /** + * Path + * @description Path to data using dot notation. Examples: 'input', 'output', 'context.user_id', 'name', 'type', '*' + * @default * + */ + path: string | null; + }; + /** + * ControlStats + * @description Aggregated statistics for a single control. + * + * Attributes: + * control_id: Database ID of the control + * control_name: Name of the control + * execution_count: Total number of executions + * match_count: Number of times the control matched + * non_match_count: Number of times the control did not match + * allow_count: Number of allow actions + * deny_count: Number of deny actions + * steer_count: Number of steer actions + * warn_count: Number of warn actions + * log_count: Number of log actions + * error_count: Number of errors during evaluation + * avg_confidence: Average confidence score + * 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 + */ + control_id: number; + /** + * Control Name + * @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 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + */ + non_match_count: number; + /** + * Steer Count + * @description Steer actions + */ + steer_count: number; + /** + * Warn Count + * @description Warn actions + */ + warn_count: number; + }; + /** + * ControlStatsResponse + * @description Response model for control-level statistics. + * + * Contains stats for a single control (with optional timeseries). + * + * Attributes: + * agent_name: Agent identifier + * time_range: Time range used + * control_id: Control ID + * control_name: Control name + * stats: Control statistics (includes timeseries when requested) + */ + ControlStatsResponse: { + /** + * Agent Name + * @description Agent identifier + */ + agent_name: string; + /** + * Control Id + * @description Control ID + */ + control_id: number; + /** + * Control Name + * @description Control name + */ + 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: { + /** + * Description + * @description Control description + */ + description?: string | null; + /** + * Enabled + * @description Whether control is enabled + * @default true + */ + enabled: boolean; + /** + * Execution + * @description 'server' or 'sdk' + */ + execution?: string | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + /** + * Stages + * @description Evaluation stages in scope + */ + stages?: string[] | null; + /** + * Step Types + * @description Step types in scope + */ + step_types?: string[] | null; + /** + * Tags + * @description Control tags + */ + tags?: string[]; + /** @description Agent using this control */ + used_by_agent?: components['schemas']['AgentRef'] | null; + /** + * Used By Agents Count + * @description Number of unique agents using this control + * @default 0 + */ + used_by_agents_count: number; + }; + /** CreateControlRequest */ + CreateControlRequest: { + /** + * Name + * @description Unique control name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreateControlResponse */ + CreateControlResponse: { + /** + * Control Id + * @description Identifier of the created control + */ + control_id: number; + }; + /** + * CreateEvaluatorConfigRequest + * @description Request to create an evaluator config template. + */ + CreateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyRequest */ + CreatePolicyRequest: { + /** + * Name + * @description Unique policy name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** CreatePolicyResponse */ + CreatePolicyResponse: { + /** + * Policy Id + * @description Identifier of the created policy + */ + policy_id: number; + }; + /** + * DeleteControlResponse + * @description Response for deleting a control. + */ + DeleteControlResponse: { + /** + * Dissociated From + * @description Deprecated: policy IDs the control was removed from before deletion + */ + dissociated_from?: number[]; + /** + * Dissociated From Agents + * @description Agent names the control was removed from before deletion + */ + dissociated_from_agents?: string[]; + /** + * Dissociated From Policies + * @description Policy IDs the control was removed from before deletion + */ + dissociated_from_policies?: number[]; + /** + * Success + * @description Whether the control was deleted + */ + success: boolean; + }; + /** + * DeleteEvaluatorConfigResponse + * @description Response for deleting an evaluator config. + */ + DeleteEvaluatorConfigResponse: { + /** + * Success + * @description Whether the evaluator config was deleted + */ + success: boolean; + }; + /** + * DeletePolicyResponse + * @description Compatibility response for singular policy deletion endpoint. + */ + DeletePolicyResponse: { + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * EvaluationRequest + * @description Request model for evaluation analysis. + * + * Used to analyze agent interactions for safety violations, + * policy compliance, and control rules. + * + * Attributes: + * 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_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "session_id": "abc123", + * "user_id": "user123" + * }, + * "input": "What is the customer's credit card number?", + * "name": "support-answer", + * "output": "I cannot share sensitive payment information.", + * "type": "llm" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "pre", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "type": "tool" + * } + * } + * @example { + * "agent_name": "customer-service-bot", + * "stage": "post", + * "step": { + * "context": { + * "user_id": "user123" + * }, + * "input": { + * "query": "SELECT * FROM users" + * }, + * "name": "search_database", + * "output": { + * "results": [] + * }, + * "type": "tool" + * } + * } + */ + EvaluationRequest: { + /** + * Agent Name + * @description Identifier of the agent making the evaluation request + */ + 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 + * @description Response model from evaluation analysis (server-side). + * + * This is what the server returns. The SDK may transform this + * into an EvaluationResult for client convenience. + * + * Attributes: + * is_safe: Whether the content is considered safe + * confidence: Confidence score between 0.0 and 1.0 + * reason: Optional explanation for the decision + * matches: List of controls that matched/triggered (if any) + * errors: List of controls that failed during evaluation (if any) + * non_matches: List of controls that were evaluated but did not match (if any) + */ + EvaluationResponse: { + /** + * Confidence + * @description Confidence score (0.0 to 1.0) + */ + confidence: number; + /** + * Errors + * @description List of controls that failed during evaluation (if any) + */ + 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; + /** + * Non Matches + * @description List of controls that were evaluated but did not match (if any) + */ + non_matches?: components['schemas']['ControlMatch'][] | null; + /** + * Reason + * @description Explanation for the decision + */ + reason?: string | null; + }; + /** + * EvaluatorConfigItem + * @description Evaluator config template stored in the server. + */ + EvaluatorConfigItem: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Created At + * @description ISO 8601 created timestamp + */ + created_at?: string | null; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Id + * @description Evaluator config ID + */ + id: number; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + /** + * Updated At + * @description ISO 8601 updated timestamp + */ + updated_at?: string | null; + }; + /** + * EvaluatorInfo + * @description Information about a registered evaluator. + */ + EvaluatorInfo: { + /** + * Config Schema + * @description JSON Schema for config + */ + 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 + */ + requires_api_key: boolean; + /** + * Timeout Ms + * @description Default timeout in milliseconds + */ + timeout_ms: number; + /** + * Version + * @description Evaluator version + */ + version: string; + }; + /** + * EvaluatorResult + * @description Result from a control evaluator. + * + * The `error` field indicates evaluator failures, NOT validation failures: + * - Set `error` for: evaluator crashes, timeouts, missing dependencies, external service errors + * - Do NOT set `error` for: invalid input, syntax errors, schema violations, constraint failures + * + * When `error` is set, `matched` must be False (fail-open on evaluator errors). + * When `error` is None, `matched` reflects the actual validation result. + * + * This distinction allows: + * - Clients to distinguish "data violated rules" from "evaluator is broken" + * - Observability systems to monitor evaluator health separately from validation outcomes + */ + EvaluatorResult: { + /** + * 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 + */ + message?: string | null; + /** + * Metadata + * @description Additional result metadata + */ + metadata?: { + [key: string]: unknown; + } | null; + }; + /** + * EvaluatorSchema + * @description Schema for a custom evaluator registered with an agent. + * + * Custom evaluators are Evaluator classes deployed with the engine. + * This schema is registered via initAgent for validation and UI purposes. + */ + EvaluatorSchema: { + /** + * Config Schema + * @description JSON Schema for evaluator config validation + */ + config_schema?: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Name + * @description Unique evaluator name + */ + name: string; + }; + /** + * EvaluatorSchemaItem + * @description Evaluator schema summary for list response. + */ + EvaluatorSchemaItem: { + /** 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 + * @description Request model for querying raw events. + * + * Supports filtering by various criteria and pagination. + * + * Attributes: + * 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_name: Filter by agent identifier + * control_ids: Filter by control IDs + * actions: Filter by actions (allow, deny, steer, 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_name": "my-agent", + * "limit": 50, + * "start_time": "2025-01-09T00:00:00Z" + * } + */ + EventQueryRequest: { + /** + * Actions + * @description Filter by actions + */ + actions?: ('allow' | 'deny' | 'steer' | 'warn' | 'log')[] | null; + /** + * Agent Name + * @description Filter by agent identifier + */ + 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; + /** + * Control Execution Id + * @description Filter by specific event ID + */ + control_execution_id?: string | null; + /** + * Control Ids + * @description Filter by control IDs + */ + control_ids?: number[] | null; + /** + * End Time + * @description Filter events before this time + */ + end_time?: string | null; + /** + * Limit + * @description Maximum events + * @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 + * @description Response model for event queries. + * + * Attributes: + * events: List of matching events + * total: Total number of matching events (for pagination) + * limit: Limit used in query + * offset: Offset used in query + */ + EventQueryResponse: { + /** + * Events + * @description Matching events + */ + events: components['schemas']['ControlExecutionEvent'][]; + /** + * Limit + * @description Limit used in query + */ + limit: number; + /** + * Offset + * @description Offset used in query + */ + offset: number; + /** + * Total + * @description Total matching events + */ + total: number; + }; + /** GetAgentPoliciesResponse */ + GetAgentPoliciesResponse: { + /** + * Policy Ids + * @description IDs of policies associated with the agent + */ + policy_ids?: number[]; + }; + /** + * GetAgentResponse + * @description Response containing agent details and registered steps. + */ + GetAgentResponse: { + /** @description Agent metadata */ + agent: components['schemas']['Agent']; + /** + * 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: { + /** @description Control data payload */ + data: components['schemas']['ControlDefinition-Output']; + }; + /** + * GetControlResponse + * @description Response containing control details. + */ + GetControlResponse: { + /** @description Control configuration data (None if not yet configured) */ + data?: components['schemas']['ControlDefinition-Output'] | null; + /** + * Id + * @description Control ID + */ + id: number; + /** + * Name + * @description Control name + */ + name: string; + }; + /** + * GetPolicyControlsResponse + * @description Response containing control IDs associated with a policy. + */ + GetPolicyControlsResponse: { + /** + * Control Ids + * @description List of control IDs associated with the policy + */ + control_ids: number[]; + }; + /** + * GetPolicyResponse + * @description Compatibility response for singular policy retrieval endpoint. + */ + GetPolicyResponse: { + /** + * Policy Id + * @description Associated policy ID + */ + policy_id: number; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components['schemas']['ValidationError'][]; + }; + /** + * HealthResponse + * @description Health check response model. + * + * Attributes: + * status: Current health status (e.g., "healthy", "degraded", "unhealthy") + * version: Application version + */ + HealthResponse: { + /** Status */ + status: string; + /** 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_name": "customer-service-bot", + * "agent_version": "1.0.0" + * }, + * "evaluators": [ + * { + * "config_schema": { + * "properties": { + * "sensitivity": { + * "type": "string" + * } + * }, + * "type": "object" + * }, + * "description": "Detects PII in text", + * "name": "pii-detector" + * } + * ], + * "steps": [ + * { + * "input_schema": { + * "query": { + * "type": "string" + * } + * }, + * "name": "search_kb", + * "output_schema": { + * "results": { + * "type": "array" + * } + * }, + * "type": "tool" + * } + * ] + * } + */ + InitAgentRequest: { + /** @description Agent metadata including ID, name, and version */ + agent: components['schemas']['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 + */ + conflict_mode: components['schemas']['ConflictMode']; + /** + * Evaluators + * @description Custom evaluator schemas for config validation + */ + evaluators?: components['schemas']['EvaluatorSchema'][]; + /** + * Force Replace + * @description If true, replace corrupted agent data instead of failing. Use only when agent data is corrupted and cannot be parsed. + * @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 + */ + controls?: components['schemas']['Control'][]; + /** + * Created + * @description True if agent was newly created, False if updated + */ + created: boolean; + /** + * Overwrite Applied + * @description True if overwrite mode changed registration data on an existing agent + * @default false + */ + overwrite_applied: boolean; + /** @description Detailed list of changes applied in overwrite mode */ + overwrite_changes?: components['schemas']['InitAgentOverwriteChanges']; + }; + JSONObject: { + [key: string]: components['schemas']['JSONValue']; + }; + /** @description Any JSON value */ + JSONValue: unknown; + /** + * ListAgentsResponse + * @description Response for listing agents. + */ + ListAgentsResponse: { + /** + * Agents + * @description List of agent summaries + */ + agents: components['schemas']['AgentSummary'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListControlsResponse + * @description Response for listing controls. + */ + ListControlsResponse: { + /** + * Controls + * @description List of control summaries + */ + controls: components['schemas']['ControlSummary'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListEvaluatorConfigsResponse + * @description Response for listing evaluator configs. + */ + ListEvaluatorConfigsResponse: { + /** + * Evaluator Configs + * @description List of evaluator configs + */ + evaluator_configs: components['schemas']['EvaluatorConfigItem'][]; + /** @description Pagination metadata */ + pagination: components['schemas']['PaginationInfo']; + }; + /** + * ListEvaluatorsResponse + * @description Response for listing agent's evaluator schemas. + */ + ListEvaluatorsResponse: { + /** Evaluators */ + evaluators: components['schemas']['EvaluatorSchemaItem'][]; + pagination: components['schemas']['PaginationInfo']; + }; + /** + * PaginationInfo + * @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; + /** + * Next Cursor + * @description Cursor for fetching the next page (null if no more pages) + */ + next_cursor?: string | null; + /** + * Total + * @description Total number of items + */ + total: number; + }; + /** + * PatchAgentRequest + * @description Request to modify an agent (remove steps/evaluators). + */ + PatchAgentRequest: { + /** + * 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: { + /** + * 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: { + /** + * Enabled + * @description Enable or disable the control + */ + enabled?: boolean | null; + /** + * Name + * @description New name for the control + */ + name?: string | null; + }; + /** + * PatchControlResponse + * @description Response from control metadata update. + */ + PatchControlResponse: { + /** + * Enabled + * @description Current enabled status (if control has data configured) + */ + enabled?: boolean | null; + /** + * Name + * @description Current control name (may have changed) + */ + name: string; + /** + * Success + * @description Whether the update succeeded + */ + success: boolean; + }; + /** + * RemoveAgentControlResponse + * @description Response for removing a direct agent-control association. + */ + RemoveAgentControlResponse: { + /** + * Control Still Active + * @description True if the control remains active via policy association(s) + */ + control_still_active: boolean; + /** + * Removed Direct Association + * @description True if a direct agent-control link was removed + */ + removed_direct_association: boolean; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * SetControlDataRequest + * @description Request to update control configuration data. + */ + SetControlDataRequest: { + /** @description Control configuration data (replaces existing) */ + data: components['schemas']['ControlDefinition-Input']; + }; + /** SetControlDataResponse */ + SetControlDataResponse: { + /** + * Success + * @description Whether the control data was updated + */ + success: boolean; + }; + /** + * SetPolicyResponse + * @description Compatibility response for singular policy assignment endpoint. + */ + SetPolicyResponse: { + /** + * Old Policy Id + * @description Previously associated policy ID, if any + */ + old_policy_id?: number | null; + /** + * Success + * @description Whether the request succeeded + */ + success: boolean; + }; + /** + * StatsResponse + * @description Response model for agent-level aggregated statistics. + * + * Contains agent-level totals (with optional timeseries) and per-control breakdown. + * + * Attributes: + * 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 Name + * @description Agent identifier + */ + agent_name: string; + /** + * Controls + * @description Per-control breakdown + */ + controls: components['schemas']['ControlStats'][]; + /** + * Time Range + * @description Time range used + */ + time_range: string; + /** @description Agent-level aggregate statistics */ + totals: components['schemas']['StatsTotals']; + }; + /** + * StatsTotals + * @description Agent-level aggregate statistics. + * + * Invariant: execution_count = match_count + non_match_count + error_count + * + * Matches have actions (allow, deny, steer, warn, log) tracked in action_counts. + * sum(action_counts.values()) == match_count + * + * Attributes: + * execution_count: Total executions across all controls + * match_count: Total matches across all controls (evaluator matched) + * non_match_count: Total non-matches across all controls (evaluator didn't match) + * error_count: Total errors across all controls (evaluation failed) + * action_counts: Breakdown of actions for matched executions + * timeseries: Time-series data points (only when include_timeseries=true) + */ + StatsTotals: { + /** + * Action Counts + * @description Action breakdown for matches: {allow, deny, steer, warn, log} + */ + action_counts?: { + [key: string]: number; + }; + /** + * Error Count + * @description Total errors + * @default 0 + */ + error_count: number; + /** + * Execution Count + * @description Total executions + */ + execution_count: number; + /** + * Match Count + * @description Total matches + * @default 0 + */ + match_count: number; + /** + * Non Match Count + * @description Total non-matches + * @default 0 + */ + non_match_count: number; + /** + * Timeseries + * @description Time-series data points (only when include_timeseries=true) + */ + timeseries?: components['schemas']['TimeseriesBucket'][] | null; + }; + /** + * SteeringContext + * @description Steering context for steer actions. + * + * This model provides an extensible structure for steering guidance. + * Future fields could include severity, categories, suggested_actions, etc. + * @example { + * "message": "This large transfer requires user verification. Request 2FA code from user, verify it, then retry the transaction with verified_2fa=True." + * } + * @example { + * "message": "Transfer exceeds daily limit. Steps: 1) Ask user for business justification, 2) Request manager approval with amount and justification, 3) If approved, retry with manager_approved=True and justification filled in." + * } + */ + SteeringContext: { + /** + * Message + * @description Guidance message explaining what needs to be corrected and how + */ + message: string; + }; + /** + * Step + * @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 Output content for this step (None for pre-checks) */ + output?: components['schemas']['JSONValue'] | null; + /** + * Type + * @description Step type (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * StepKey + * @description Identifies a registered step schema by type and name. + */ + StepKey: { + /** + * Name + * @description Registered step name + */ + name: string; + /** + * Type + * @description Step type + */ + type: string; + }; + /** + * StepSchema + * @description Schema for a registered agent step. + * @example { + * "description": "Search the internal knowledge base", + * "input_schema": { + * "query": { + * "description": "Search query", + * "type": "string" + * } + * }, + * "name": "search_knowledge_base", + * "output_schema": { + * "results": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "type": "tool" + * } + * @example { + * "description": "Customer support response generation", + * "input_schema": { + * "messages": { + * "items": { + * "type": "object" + * }, + * "type": "array" + * } + * }, + * "name": "support-answer", + * "output_schema": { + * "text": { + * "type": "string" + * } + * }, + * "type": "llm" + * } + */ + StepSchema: { + /** + * Description + * @description Optional description of the step + */ + description?: string | null; + /** + * Input Schema + * @description JSON schema describing step input + */ + 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 + */ + output_schema?: { + [key: string]: unknown; + } | null; + /** + * Type + * @description Step type for this schema (e.g., 'tool', 'llm') + */ + type: string; + }; + /** + * TimeseriesBucket + * @description Single data point in a time-series. + * + * Represents aggregated metrics for a single time bucket. + * + * Attributes: + * timestamp: Start time of the bucket (UTC, always timezone-aware) + * execution_count: Total executions in this bucket + * match_count: Number of matches in this bucket + * non_match_count: Number of non-matches in this bucket + * error_count: Number of errors in this bucket + * action_counts: Breakdown of actions for matched executions + * avg_confidence: Average confidence score (None if no executions) + * avg_duration_ms: Average execution duration in milliseconds (None if no data) + */ + TimeseriesBucket: { + /** + * Action Counts + * @description Action breakdown: {allow, deny, steer, warn, log} + */ + 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 + */ + execution_count: number; + /** + * Match Count + * @description Matches in bucket + */ + match_count: number; + /** + * Non Match Count + * @description Non-matches in bucket + */ + non_match_count: number; + /** + * Timestamp + * Format: date-time + * @description Start time of the bucket (UTC) + */ + timestamp: string; + }; + /** + * UpdateEvaluatorConfigRequest + * @description Request to replace an evaluator config template. + */ + UpdateEvaluatorConfigRequest: { + /** + * Config + * @description Evaluator-specific configuration + */ + config: { + [key: string]: unknown; + }; + /** + * Description + * @description Optional description + */ + description?: string | null; + /** + * Evaluator + * @description Evaluator name (built-in or custom) + */ + evaluator: string; + /** + * Name + * @description Unique evaluator config name (letters, numbers, hyphens, underscores) + */ + name: string; + }; + /** + * ValidateControlDataRequest + * @description Request to validate control configuration data without saving. + */ + ValidateControlDataRequest: { + /** @description Control configuration data to validate */ + data: components['schemas']['ControlDefinition-Input']; + }; + /** 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 */ + msg: string; + /** Error Type */ + type: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - list_agents_api_v1_agents_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - name?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of agent summaries */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListAgentsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - init_agent_api_v1_agents_initAgent_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["InitAgentRequest"]; - }; - }; - responses: { - /** @description Agent registration status with active controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["InitAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_agent_api_v1_agents__agent_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Agent metadata and registered steps */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - patch_agent_api_v1_agents__agent_name__patch: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["PatchAgentRequest"]; - }; - }; - responses: { - /** @description Lists of removed items */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PatchAgentResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - list_agent_controls_api_v1_agents__agent_name__controls_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of controls from agent policy and direct associations */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["AgentControlsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - add_agent_control_api_v1_agents__agent_name__controls__control_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - 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_agent_control_api_v1_agents__agent_name__controls__control_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["RemoveAgentControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { - parameters: { - query?: { - cursor?: string | null; - limit?: number; - }; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator schemas registered with this agent */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListEvaluatorsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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"]; - }; - }; - }; - }; - get_agent_policies_api_v1_agents__agent_name__policies_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of policy IDs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetAgentPoliciesResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - remove_all_agent_policies_api_v1_agents__agent_name__policies_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - }; - 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"]; - }; - }; - }; - }; - add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - policy_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_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: string; - policy_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"]; - }; - }; - }; - }; - get_agent_policy_api_v1_agents__agent_name__policy_get: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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_name__policy_delete: { - parameters: { - query?: never; - header?: never; - path: { - agent_name: 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"]; - }; - }; - }; - }; - 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 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"]; - }; - }; - }; - }; - list_controls_api_v1_controls_get: { - parameters: { - query?: { - /** @description Control ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by enabled status */ - enabled?: boolean | null; - /** @description Filter by step type (built-ins: 'tool', 'llm') */ - step_type?: string | null; - /** @description Filter by stage ('pre' or 'post') */ - stage?: string | null; - /** @description Filter by execution ('server' or 'sdk') */ - execution?: string | null; - /** @description Filter by tag */ - tag?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of controls */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListControlsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - create_control_api_v1_controls_put: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateControlRequest"]; - }; - }; - responses: { - /** @description Created control ID */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreateControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - 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; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control metadata and configuration */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - delete_control_api_v1_controls__control_id__delete: { - parameters: { - query?: { - /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ - force?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation with dissociation info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - patch_control_api_v1_controls__control_id__patch: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["PatchControlRequest"]; - }; - }; - responses: { - /** @description Updated control information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PatchControlResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_control_data_api_v1_controls__control_id__data_get: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Control data payload */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["GetControlDataResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - set_control_data_api_v1_controls__control_id__data_put: { - parameters: { - query?: never; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetControlDataRequest"]; - }; - }; - responses: { - /** @description Success confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SetControlDataResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - 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?: { - /** @description Evaluator config ID to start after */ - cursor?: number | null; - limit?: number; - /** @description Filter by name (partial, case-insensitive) */ - name?: string | null; - /** @description Filter by evaluator name */ - evaluator?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Paginated list of evaluator configs */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ListEvaluatorConfigsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - create_evaluator_config_api_v1_evaluator_configs_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateEvaluatorConfigRequest"]; - }; - }; - responses: { - /** @description Created evaluator config */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_evaluator_config_api_v1_evaluator_configs__config_id__get: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Evaluator config details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - update_evaluator_config_api_v1_evaluator_configs__config_id__put: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateEvaluatorConfigRequest"]; - }; - }; - responses: { - /** @description Updated evaluator config */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EvaluatorConfigItem"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { - parameters: { - query?: never; - header?: never; - path: { - config_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deletion confirmation */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeleteEvaluatorConfigResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_evaluators_api_v1_evaluators_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Dictionary of evaluator name to evaluator info */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: components["schemas"]["EvaluatorInfo"]; - }; - }; - }; - }; - }; - ingest_events_api_v1_observability_events_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["BatchEventsRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BatchEventsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - query_events_api_v1_observability_events_query_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["EventQueryRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["EventQueryResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_stats_api_v1_observability_stats_get: { - parameters: { - query: { - agent_name: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; - include_timeseries?: boolean; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["StatsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_control_stats_api_v1_observability_stats_controls__control_id__get: { - parameters: { - query: { - agent_name: string; - time_range?: "1m" | "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "180d" | "365d"; - include_timeseries?: boolean; - }; - header?: never; - path: { - control_id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ControlStatsResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_status_api_v1_observability_status_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: unknown; - }; - }; - }; - }; - }; - 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; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Server health status */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HealthResponse"]; - }; - }; + list_agents_api_v1_agents_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + name?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of agent summaries */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListAgentsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + init_agent_api_v1_agents_initAgent_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['InitAgentRequest']; + }; + }; + responses: { + /** @description Agent registration status with active controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['InitAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_agent_api_v1_agents__agent_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Agent metadata and registered steps */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + patch_agent_api_v1_agents__agent_name__patch: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['PatchAgentRequest']; + }; + }; + responses: { + /** @description Lists of removed items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PatchAgentResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_agent_controls_api_v1_agents__agent_name__controls_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of controls from agent policy and direct associations */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AgentControlsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + add_agent_control_api_v1_agents__agent_name__controls__control_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + 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_agent_control_api_v1_agents__agent_name__controls__control_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['RemoveAgentControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + list_agent_evaluators_api_v1_agents__agent_name__evaluators_get: { + parameters: { + query?: { + cursor?: string | null; + limit?: number; + }; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator schemas registered with this agent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListEvaluatorsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_agent_evaluator_api_v1_agents__agent_name__evaluators__evaluator_name__get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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']; + }; + }; + }; + }; + get_agent_policies_api_v1_agents__agent_name__policies_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of policy IDs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetAgentPoliciesResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + remove_all_agent_policies_api_v1_agents__agent_name__policies_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + }; + 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']; + }; + }; + }; + }; + add_agent_policy_api_v1_agents__agent_name__policies__policy_id__post: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_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_agent_policy_api_v1_agents__agent_name__policies__policy_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: string; + policy_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']; + }; + }; + }; + }; + get_agent_policy_api_v1_agents__agent_name__policy_get: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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_name__policy_delete: { + parameters: { + query?: never; + header?: never; + path: { + agent_name: 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']; + }; + }; + }; + }; + 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 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']; + }; + }; + }; + }; + list_controls_api_v1_controls_get: { + parameters: { + query?: { + /** @description Control ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by enabled status */ + enabled?: boolean | null; + /** @description Filter by step type (built-ins: 'tool', 'llm') */ + step_type?: string | null; + /** @description Filter by stage ('pre' or 'post') */ + stage?: string | null; + /** @description Filter by execution ('server' or 'sdk') */ + execution?: string | null; + /** @description Filter by tag */ + tag?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of controls */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListControlsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + create_control_api_v1_controls_put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateControlRequest']; + }; + }; + responses: { + /** @description Created control ID */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['CreateControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + 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; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control metadata and configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + delete_control_api_v1_controls__control_id__delete: { + parameters: { + query?: { + /** @description If true, dissociate from all policy/agent links before deleting. If false, fail if control is associated with any policy or agent. */ + force?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation with dissociation info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeleteControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + patch_control_api_v1_controls__control_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['PatchControlRequest']; + }; + }; + responses: { + /** @description Updated control information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PatchControlResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_control_data_api_v1_controls__control_id__data_get: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Control data payload */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetControlDataResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + set_control_data_api_v1_controls__control_id__data_put: { + parameters: { + query?: never; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['SetControlDataRequest']; + }; + }; + responses: { + /** @description Success confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SetControlDataResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + 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?: { + /** @description Evaluator config ID to start after */ + cursor?: number | null; + limit?: number; + /** @description Filter by name (partial, case-insensitive) */ + name?: string | null; + /** @description Filter by evaluator name */ + evaluator?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of evaluator configs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ListEvaluatorConfigsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + create_evaluator_config_api_v1_evaluator_configs_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateEvaluatorConfigRequest']; + }; + }; + responses: { + /** @description Created evaluator config */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_evaluator_config_api_v1_evaluator_configs__config_id__get: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Evaluator config details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + update_evaluator_config_api_v1_evaluator_configs__config_id__put: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdateEvaluatorConfigRequest']; + }; + }; + responses: { + /** @description Updated evaluator config */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EvaluatorConfigItem']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + delete_evaluator_config_api_v1_evaluator_configs__config_id__delete: { + parameters: { + query?: never; + header?: never; + path: { + config_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion confirmation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeleteEvaluatorConfigResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_evaluators_api_v1_evaluators_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Dictionary of evaluator name to evaluator info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: components['schemas']['EvaluatorInfo']; + }; + }; + }; + }; + }; + ingest_events_api_v1_observability_events_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['BatchEventsRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BatchEventsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + query_events_api_v1_observability_events_query_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EventQueryRequest']; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['EventQueryResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_stats_api_v1_observability_stats_get: { + parameters: { + query: { + agent_name: string; + time_range?: + | '1m' + | '5m' + | '15m' + | '1h' + | '24h' + | '7d' + | '30d' + | '180d' + | '365d'; + include_timeseries?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['StatsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_control_stats_api_v1_observability_stats_controls__control_id__get: { + parameters: { + query: { + agent_name: string; + time_range?: + | '1m' + | '5m' + | '15m' + | '1h' + | '24h' + | '7d' + | '30d' + | '180d' + | '365d'; + include_timeseries?: boolean; + }; + header?: never; + path: { + control_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ControlStatsResponse']; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPValidationError']; + }; + }; + }; + }; + get_status_api_v1_observability_status_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: unknown; + }; + }; + }; + }; + }; + 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; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Server health status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HealthResponse']; }; + }; }; + }; } diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index b61160e2..5ac120a6 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -80,7 +80,9 @@ export type ControlDefinitionInput = components['schemas']['ControlDefinition-Input']; export type ControlDefinitionOutput = components['schemas']['ControlDefinition-Output']; -export type ControlDefinition = ControlDefinitionInput | ControlDefinitionOutput; +export type ControlDefinition = + | ControlDefinitionInput + | ControlDefinitionOutput; export type Control = components['schemas']['Control']; export type AgentControlsResponse = components['schemas']['AgentControlsResponse']; diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 928bd830..020b2f3a 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -27,8 +27,8 @@ export function useDeleteControlFlow({ children: ( Remove "{control.name}" from this agent? This only removes - the direct association from this agent and does not delete the - control globally. + the direct association from this agent and does not delete the control + globally. ), labels: { confirm: 'Remove', cancel: 'Cancel' }, diff --git a/ui/tests/agent-stats.spec.ts b/ui/tests/agent-stats.spec.ts index 8791e58f..872a251d 100644 --- a/ui/tests/agent-stats.spec.ts +++ b/ui/tests/agent-stats.spec.ts @@ -6,7 +6,7 @@ test.describe('Agent Monitor Tab', () => { await mockedPage.goto('/agents/agent-1/monitor'); // Wait for the page to load await expect( - mockedPage.getByRole('heading', { name: 'Customer Support Bot' }) + mockedPage.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); }); @@ -150,7 +150,7 @@ test.describe('Agent Monitor Tab - Empty State', () => { // Navigate to agent detail page await page.goto('/agents/agent-1/monitor'); await expect( - page.getByRole('heading', { name: 'Customer Support Bot' }) + page.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); // Navigate to stats tab @@ -239,7 +239,7 @@ test.describe('Agent Monitor Tab - Refetch Flow', () => { // Navigate to agent detail page await page.goto('/agents/agent-1/monitor'); await expect( - page.getByRole('heading', { name: 'Customer Support Bot' }) + page.getByRole('heading', { name: 'customer-support-bot' }) ).toBeVisible(); // Navigate to stats tab @@ -276,7 +276,7 @@ test.describe('Agent Monitor Tab - Error State', () => { // Navigate to agent detail page await page.goto('/agents/agent-1/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 8b48d0cc..d18b6c28 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -58,16 +58,19 @@ test.describe('Control Store Modal', () => { } }); - test('displays usage counts in Used by column', async ({ mockedPage }) => { + test('displays usage attribution in Used by column', async ({ + mockedPage, + }) => { const modal = await openControlStoreModal(mockedPage); - // Two controls are used by one agent each in fixture data - await expect(modal.getByText('1 agent')).toHaveCount(2); + // Two controls show direct agent-name attribution in fixture data. + await expect(modal.getByText('customer-support-bot')).toBeVisible(); + await expect(modal.getByText('data-analysis-agent')).toBeVisible(); // One control has no usage and renders as an em dash await expect(modal.getByText('—')).toBeVisible(); - // Agent links are no longer rendered in this column + // Attribution is plain text, not links. await expect( - modal.getByRole('link', { name: 'Customer Support Bot' }) + modal.getByRole('link', { name: 'customer-support-bot' }) ).toHaveCount(0); }); diff --git a/ui/tests/fixtures.ts b/ui/tests/fixtures.ts index 39f0aa86..04263ed5 100644 --- a/ui/tests/fixtures.ts +++ b/ui/tests/fixtures.ts @@ -21,7 +21,6 @@ import type { StatsResponse } from '@/core/hooks/query-hooks/use-agent-monitor'; const agentsList: AgentSummary[] = [ { agent_name: 'customer-support-bot', - policy_id: 1, policy_ids: [1], created_at: '2024-01-01T00:00:00Z', step_count: 5, @@ -30,7 +29,6 @@ const agentsList: AgentSummary[] = [ }, { agent_name: 'data-analysis-agent', - policy_id: 2, policy_ids: [2], created_at: '2024-01-02T00:00:00Z', step_count: 3, @@ -39,7 +37,6 @@ const agentsList: AgentSummary[] = [ }, { agent_name: 'code-review-assistant', - policy_id: 3, policy_ids: [3], created_at: '2024-01-03T00:00:00Z', step_count: 8, diff --git a/ui/tests/home.spec.ts b/ui/tests/home.spec.ts index f1c75adc..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, }); diff --git a/ui/tests/search-input.spec.ts b/ui/tests/search-input.spec.ts index 61a6bdeb..92b48b40 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,10 @@ 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\/monitor/ + ); // Go back await mockedPage.goBack(); @@ -105,7 +107,7 @@ 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(); }); }); From ac59b04ee1b1b1d60bc3fd26f8c66a5f88f4fc3a Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 17:12:57 -0800 Subject: [PATCH 27/32] test: add multi-policy additive coverage and fix server docs --- server/README.md | 2 +- .../agent_control_server/endpoints/agents.py | 2 +- server/tests/test_policy_integration.py | 55 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/server/README.md b/server/README.md index 6abcbb71..05780231 100644 --- a/server/README.md +++ b/server/README.md @@ -157,7 +157,7 @@ GET /api/v1/evaluators ```bash # Register or update agent -POST /api/v1/agents/init +POST /api/v1/agents/initAgent Body: { "agent": {...}, "tools": [...], "force_replace": false } # Get agent diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index bf031e55..c11e3de7 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -426,7 +426,7 @@ async def init_agent( db: Database session (injected) Returns: - InitAgentResponse with created flag and active controls (if policy assigned) + InitAgentResponse with created flag and active controls (policy-derived + direct) """ # Check for evaluator name collisions with built-in evaluators builtin_names = _get_builtin_evaluator_names() diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index b556436f..5fb87edf 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -294,6 +294,61 @@ def test_control_shared_between_policies(client: TestClient) -> None: assert control_id in {c["id"] for c in resp_b.json()["controls"]} +def test_agent_controls_union_across_multiple_policies_with_dedupe(client: TestClient) -> None: + """Agent controls should be additive across policies and deduplicated by control id.""" + agent_name, _ = _create_agent(client) + policy_a_id = _create_policy(client, "policy-a") + policy_b_id = _create_policy(client, "policy-b") + + shared_control_id = _create_control(client) + policy_a_only_control_id = _create_control(client) + policy_b_only_control_id = _create_control(client) + + # Policy A: shared + A-only + resp = client.post(f"/api/v1/policies/{policy_a_id}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/policies/{policy_a_id}/controls/{policy_a_only_control_id}") + assert resp.status_code == 200 + + # Policy B: shared + B-only + resp = client.post(f"/api/v1/policies/{policy_b_id}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/policies/{policy_b_id}/controls/{policy_b_only_control_id}") + assert resp.status_code == 200 + + # Associate both policies with the same agent (plural additive endpoint). + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_a_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_b_id}") + assert resp.status_code == 200 + + # Agent should have both policy associations. + policies_resp = client.get(f"/api/v1/agents/{agent_name}/policies") + assert policies_resp.status_code == 200 + assert policies_resp.json()["policy_ids"] == [policy_a_id, policy_b_id] + + # Active controls should be union(policy A, policy B) with shared deduped. + controls_resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert controls_resp.status_code == 200 + controls = controls_resp.json()["controls"] + received_control_ids = {control["id"] for control in controls} + assert received_control_ids == { + shared_control_id, + policy_a_only_control_id, + policy_b_only_control_id, + } + assert len(controls) == 3 + + # list_agents count should match the deduplicated union. + agents_resp = client.get("/api/v1/agents", params={"name": agent_name}) + assert agents_resp.status_code == 200 + matching_agents = [ + agent for agent in agents_resp.json()["agents"] if agent["agent_name"] == agent_name + ] + assert len(matching_agents) == 1 + assert matching_agents[0]["active_controls_count"] == 3 + + def test_agent_gets_controls_from_direct_associations(client: TestClient) -> None: """Agent should see controls directly associated with it.""" agent_name, _ = _create_agent(client) From 9a66aefae0b2eedc9b348dc85da98e9252955b56 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 18:10:22 -0800 Subject: [PATCH 28/32] fix: address multi-policy review findings and test gaps --- sdks/python/src/agent_control/__init__.py | 3 +- sdks/python/src/agent_control/policies.py | 10 +- .../agent_control_server/endpoints/agents.py | 2 +- .../endpoints/controls.py | 23 ++- server/tests/test_controls_additional.py | 98 ++++++++++- server/tests/test_policy_integration.py | 162 ++++++++++++++++++ ...ol.ts => use-remove-control-from-agent.ts} | 0 .../agent-detail/controls/table-columns.tsx | 2 +- .../controls/use-delete-control-flow.tsx | 2 +- 9 files changed, 287 insertions(+), 15 deletions(-) rename ui/src/core/hooks/query-hooks/{use-delete-control.ts => use-remove-control-from-agent.ts} (100%) diff --git a/sdks/python/src/agent_control/__init__.py b/sdks/python/src/agent_control/__init__.py index 5aba318c..ce5286b2 100644 --- a/sdks/python/src/agent_control/__init__.py +++ b/sdks/python/src/agent_control/__init__.py @@ -18,7 +18,8 @@ async def chat(message: str) -> str: return await assistant.respond(message) - # Apply all controls for this agent + # Optional policy label for grouping/readability; control selection still follows + # the server's active controls (policy + direct associations). @agent_control.control(policy="safety-policy") async def process(input: str) -> str: return await pipeline.run(input) diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index 0f5f913d..cd9a6323 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -2,8 +2,8 @@ from typing import Any, cast +from . import agents from .client import AgentControlClient -from .validation import ensure_agent_name async def create_policy( @@ -171,9 +171,5 @@ async def assign_policy_to_agent( httpx.HTTPError: If request fails HTTPException 404: Agent or policy not found """ - agent_name_str = ensure_agent_name(agent_name) - response = await client.http_client.post( - f"/api/v1/agents/{agent_name_str}/policies/{policy_id}" - ) - response.raise_for_status() - return cast(dict[str, Any], response.json()) + # Keep policy module API while delegating to the canonical agents helper. + return await agents.add_agent_policy(client, agent_name, policy_id) diff --git a/server/src/agent_control_server/endpoints/agents.py b/server/src/agent_control_server/endpoints/agents.py index c11e3de7..423d38db 100644 --- a/server/src/agent_control_server/endpoints/agents.py +++ b/server/src/agent_control_server/endpoints/agents.py @@ -180,7 +180,7 @@ async def _build_overwrite_evaluator_removals( ) -> list[InitAgentEvaluatorRemoval]: """Build evaluator removal details, including active-control references.""" if not removed_evaluators: - return [InitAgentEvaluatorRemoval(name=name) for name in sorted(removed_evaluators)] + return [] try: controls = await list_controls_for_agent( diff --git a/server/src/agent_control_server/endpoints/controls.py b/server/src/agent_control_server/endpoints/controls.py index 12069c7e..e0d5b805 100644 --- a/server/src/agent_control_server/endpoints/controls.py +++ b/server/src/agent_control_server/endpoints/controls.py @@ -22,6 +22,7 @@ from jsonschema_rs import ValidationError as JSONSchemaValidationError from pydantic import ValidationError from sqlalchemy import Integer, String, delete, func, literal, or_, select, union_all +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from ..db import get_async_db @@ -236,6 +237,15 @@ async def create_control( try: await db.commit() await db.refresh(control) + except IntegrityError: + await db.rollback() + raise ConflictError( + error_code=ErrorCode.CONTROL_NAME_CONFLICT, + detail=f"Control with name '{request.name}' already exists", + resource="Control", + resource_id=request.name, + hint="Choose a different name or update the existing control.", + ) except Exception: await db.rollback() _logger.error( @@ -497,14 +507,11 @@ async def list_controls( Example: GET /controls?limit=10&enabled=true&step_type=tool """ - # Get total count (with filters applied) - count_query = select(func.count()).select_from(Control) query = select(Control).order_by(Control.id.desc()) # Apply cursor if cursor is not None: query = query.where(Control.id < cursor) - count_query = count_query.where(Control.id < cursor) # Apply name filter (case-insensitive partial match) if name is not None: @@ -943,6 +950,16 @@ async def patch_control( try: await db.commit() _logger.info(f"Updated control '{control.name}' ({control_id})") + except IntegrityError: + await db.rollback() + conflicting_name = request.name or control.name + raise ConflictError( + error_code=ErrorCode.CONTROL_NAME_CONFLICT, + detail=f"Control with name '{conflicting_name}' already exists", + resource="Control", + resource_id=conflicting_name, + hint="Choose a different name or update the existing control.", + ) except Exception: await db.rollback() _logger.error( diff --git a/server/tests/test_controls_additional.py b/server/tests/test_controls_additional.py index 4743b6b5..21a58f5f 100644 --- a/server/tests/test_controls_additional.py +++ b/server/tests/test_controls_additional.py @@ -2,19 +2,24 @@ import json import uuid +from collections.abc import AsyncGenerator from copy import deepcopy from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock import pytest from fastapi.testclient import TestClient from sqlalchemy import text +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session +from agent_control_server.db import get_async_db from agent_control_server.models import Control from agent_control_evaluators import RegexEvaluatorConfig from agent_control_server.endpoints import controls as controls_module -from agent_control_server.models import Control +from agent_control_server.main import app from .conftest import engine from .utils import VALID_CONTROL_PAYLOAD @@ -32,6 +37,71 @@ def _set_control_data(client: TestClient, control_id: int, data: dict) -> None: assert resp.status_code == 200, resp.text +def test_create_control_integrity_error_returns_conflict(client: TestClient) -> None: + """DB uniqueness violations during create should be surfaced as 409 conflicts.""" + + async def mock_db_integrity_error() -> AsyncGenerator[AsyncSession, None]: + mock_session = AsyncMock(spec=AsyncSession) + existing_result = MagicMock() + existing_result.first.return_value = None + + mock_session.execute = AsyncMock(return_value=existing_result) + mock_session.add = MagicMock() + mock_session.refresh = AsyncMock() + mock_session.commit = AsyncMock( + side_effect=IntegrityError( + "INSERT INTO controls ...", + {"name": "duplicate-control"}, + Exception("duplicate key value violates unique constraint"), + ) + ) + yield mock_session + + app.dependency_overrides[get_async_db] = mock_db_integrity_error + try: + resp = client.put("/api/v1/controls", json={"name": "duplicate-control"}) + finally: + app.dependency_overrides.clear() + + assert resp.status_code == 409 + assert resp.json()["error_code"] == "CONTROL_NAME_CONFLICT" + + +def test_patch_control_rename_integrity_error_returns_conflict(client: TestClient) -> None: + """DB uniqueness violations during rename should be surfaced as 409 conflicts.""" + control_obj = SimpleNamespace(id=1, name="old-control", data={}) + + async def mock_db_integrity_error() -> AsyncGenerator[AsyncSession, None]: + mock_session = AsyncMock(spec=AsyncSession) + + control_lookup_result = MagicMock() + control_lookup_result.scalars.return_value.first.return_value = control_obj + + name_lookup_result = MagicMock() + name_lookup_result.first.return_value = None + + mock_session.execute = AsyncMock( + side_effect=[control_lookup_result, name_lookup_result] + ) + mock_session.commit = AsyncMock( + side_effect=IntegrityError( + "UPDATE controls ...", + {"name": "existing-control"}, + Exception("duplicate key value violates unique constraint"), + ) + ) + yield mock_session + + app.dependency_overrides[get_async_db] = mock_db_integrity_error + try: + resp = client.patch("/api/v1/controls/1", json={"name": "existing-control"}) + finally: + app.dependency_overrides.clear() + + assert resp.status_code == 409 + assert resp.json()["error_code"] == "CONTROL_NAME_CONFLICT" + + def test_list_controls_filters_and_pagination(client: TestClient) -> None: # Given: three controls with varying data control1_id, control1_name = _create_control(client, name=f"AlphaControl-{uuid.uuid4()}") @@ -425,6 +495,32 @@ def test_delete_control_force_dissociates(client: TestClient) -> None: assert control_id not in list_resp.json()["control_ids"] +def test_delete_control_force_dissociates_direct_agent_links(client: TestClient) -> None: + # Given: a control directly associated with an agent + control_id, _ = _create_control(client) + _set_control_data(client, control_id, deepcopy(VALID_CONTROL_PAYLOAD)) + + 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 + + assoc_resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert assoc_resp.status_code == 200 + + # When: force-deleting the control + resp = client.delete(f"/api/v1/controls/{control_id}?force=true") + assert resp.status_code == 200 + body = resp.json() + + # Then: direct agent dissociation details are returned + assert body["success"] is True + assert body.get("dissociated_from_policies", []) == [] + assert body.get("dissociated_from_agents", []) == [agent_name] + + def test_get_control_corrupted_data_returns_none(client: TestClient) -> None: # Given: a control with corrupted data in DB control_id, control_name = _create_control(client) diff --git a/server/tests/test_policy_integration.py b/server/tests/test_policy_integration.py index 5fb87edf..bd3ba15d 100644 --- a/server/tests/test_policy_integration.py +++ b/server/tests/test_policy_integration.py @@ -349,6 +349,168 @@ def test_agent_controls_union_across_multiple_policies_with_dedupe(client: TestC assert matching_agents[0]["active_controls_count"] == 3 +def test_remove_one_policy_keeps_controls_from_remaining_policies(client: TestClient) -> None: + """Removing one policy should preserve controls inherited from other policies.""" + agent_name, _ = _create_agent(client) + policy_a_id = _create_policy(client, "policy-a") + policy_b_id = _create_policy(client, "policy-b") + + shared_control_id = _create_control(client) + policy_a_only_control_id = _create_control(client) + policy_b_only_control_id = _create_control(client) + + # Policy A: shared + A-only + resp = client.post(f"/api/v1/policies/{policy_a_id}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/policies/{policy_a_id}/controls/{policy_a_only_control_id}") + assert resp.status_code == 200 + + # Policy B: shared + B-only + resp = client.post(f"/api/v1/policies/{policy_b_id}/controls/{shared_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/policies/{policy_b_id}/controls/{policy_b_only_control_id}") + assert resp.status_code == 200 + + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_a_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_b_id}") + assert resp.status_code == 200 + + # Remove only policy A. + resp = client.delete(f"/api/v1/agents/{agent_name}/policies/{policy_a_id}") + assert resp.status_code == 200 + + policies_resp = client.get(f"/api/v1/agents/{agent_name}/policies") + assert policies_resp.status_code == 200 + assert policies_resp.json()["policy_ids"] == [policy_b_id] + + controls_resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert controls_resp.status_code == 200 + received_control_ids = {control["id"] for control in controls_resp.json()["controls"]} + assert received_control_ids == {shared_control_id, policy_b_only_control_id} + + +def test_remove_all_policies_preserves_direct_controls(client: TestClient) -> None: + """Removing all policy links should keep direct agent-control associations active.""" + agent_name, _ = _create_agent(client) + policy_id = _create_policy(client) + policy_control_id = _create_control(client) + direct_control_id = _create_control(client) + + resp = client.post(f"/api/v1/policies/{policy_id}/controls/{policy_control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{direct_control_id}") + assert resp.status_code == 200 + + resp = client.delete(f"/api/v1/agents/{agent_name}/policies") + assert resp.status_code == 200 + assert resp.json()["success"] is True + + policies_resp = client.get(f"/api/v1/agents/{agent_name}/policies") + assert policies_resp.status_code == 200 + assert policies_resp.json()["policy_ids"] == [] + + controls_resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert controls_resp.status_code == 200 + assert {control["id"] for control in controls_resp.json()["controls"]} == {direct_control_id} + + +def test_add_agent_policy_is_idempotent(client: TestClient) -> None: + """Adding the same policy association twice should not duplicate links.""" + agent_name, _ = _create_agent(client) + policy_id = _create_policy(client) + + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/policies/{policy_id}") + assert resp.status_code == 200 + + policies_resp = client.get(f"/api/v1/agents/{agent_name}/policies") + assert policies_resp.status_code == 200 + assert policies_resp.json()["policy_ids"] == [policy_id] + + +def test_add_agent_control_is_idempotent(client: TestClient) -> None: + """Adding the same direct control twice should not duplicate active controls.""" + agent_name, _ = _create_agent(client) + control_id = _create_control(client) + + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + resp = client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + assert resp.status_code == 200 + + controls_resp = client.get(f"/api/v1/agents/{agent_name}/controls") + assert controls_resp.status_code == 200 + controls = controls_resp.json()["controls"] + assert {control["id"] for control in controls} == {control_id} + assert len(controls) == 1 + + +def test_agent_policy_endpoints_return_404_for_missing_resources(client: TestClient) -> None: + """Plural policy endpoints should return consistent 404s for missing agent/policy.""" + existing_agent_name, _ = _create_agent(client) + existing_policy_id = _create_policy(client) + + missing_agent_name = "missing-agent-1234" + missing_policy_id = 999999 + + # Missing agent on add/list/remove-one/remove-all. + resp = client.post(f"/api/v1/agents/{missing_agent_name}/policies/{existing_policy_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + resp = client.get(f"/api/v1/agents/{missing_agent_name}/policies") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + resp = client.delete(f"/api/v1/agents/{missing_agent_name}/policies/{existing_policy_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + resp = client.delete(f"/api/v1/agents/{missing_agent_name}/policies") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + # Missing policy on add/remove-one. + resp = client.post(f"/api/v1/agents/{existing_agent_name}/policies/{missing_policy_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "POLICY_NOT_FOUND" + + resp = client.delete(f"/api/v1/agents/{existing_agent_name}/policies/{missing_policy_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "POLICY_NOT_FOUND" + + +def test_agent_control_endpoints_return_404_for_missing_resources(client: TestClient) -> None: + """Direct control association endpoints should return 404s for missing agent/control.""" + existing_agent_name, _ = _create_agent(client) + existing_control_id = _create_control(client) + + missing_agent_name = "missing-agent-1234" + missing_control_id = 999999 + + # Missing agent on add/remove. + resp = client.post(f"/api/v1/agents/{missing_agent_name}/controls/{existing_control_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + resp = client.delete(f"/api/v1/agents/{missing_agent_name}/controls/{existing_control_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "AGENT_NOT_FOUND" + + # Missing control on add/remove. + resp = client.post(f"/api/v1/agents/{existing_agent_name}/controls/{missing_control_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "CONTROL_NOT_FOUND" + + resp = client.delete(f"/api/v1/agents/{existing_agent_name}/controls/{missing_control_id}") + assert resp.status_code == 404 + assert resp.json()["error_code"] == "CONTROL_NOT_FOUND" + + def test_agent_gets_controls_from_direct_associations(client: TestClient) -> None: """Agent should see controls directly associated with it.""" agent_name, _ = _create_agent(client) diff --git a/ui/src/core/hooks/query-hooks/use-delete-control.ts b/ui/src/core/hooks/query-hooks/use-remove-control-from-agent.ts similarity index 100% rename from ui/src/core/hooks/query-hooks/use-delete-control.ts rename to ui/src/core/hooks/query-hooks/use-remove-control-from-agent.ts diff --git a/ui/src/core/page-components/agent-detail/controls/table-columns.tsx b/ui/src/core/page-components/agent-detail/controls/table-columns.tsx index 21036415..745358a3 100644 --- a/ui/src/core/page-components/agent-detail/controls/table-columns.tsx +++ b/ui/src/core/page-components/agent-detail/controls/table-columns.tsx @@ -6,7 +6,7 @@ import { type ColumnDef } from '@tanstack/react-table'; import { useMemo } from 'react'; import type { Control } from '@/core/api/types'; -import type { useRemoveControlFromAgent } from '@/core/hooks/query-hooks/use-delete-control'; +import type { useRemoveControlFromAgent } from '@/core/hooks/query-hooks/use-remove-control-from-agent'; import type { useUpdateControl } from '@/core/hooks/query-hooks/use-update-control'; import { getStepTypeLabelAndColor } from './utils'; diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 020b2f3a..0c6929bc 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -6,7 +6,7 @@ import type { Control } from '@/core/api/types'; import { type RemoveControlFromAgentResult, useRemoveControlFromAgent, -} from '@/core/hooks/query-hooks/use-delete-control'; +} from '@/core/hooks/query-hooks/use-remove-control-from-agent'; type UseDeleteControlFlowParams = { agentId: string; From 7bc1808be6e7dd23cf3b982f67a4b260f93f1e0e Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 18:18:55 -0800 Subject: [PATCH 29/32] fix: stabilize sdk-ts CI generate checks --- sdks/typescript/Makefile | 7 ++++++- sdks/typescript/src/generated/funcs/agents-init.ts | 2 +- sdks/typescript/src/generated/sdk/agents.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sdks/typescript/Makefile b/sdks/typescript/Makefile index 351e37b1..70b6708c 100644 --- a/sdks/typescript/Makefile +++ b/sdks/typescript/Makefile @@ -34,7 +34,12 @@ overlay-test: cd ../.. && uv run --package agent-control-server pytest sdks/typescript/tests/test_generate_method_names_overlay.py name-check: - @if rg -n --no-heading 'async\\s+[A-Za-z0-9]+(ApiV[0-9]+|Get|Post|Put|Patch|Delete)\\b' src/generated/sdk; then \ + @if command -v rg >/dev/null 2>&1; then \ + MATCH_CMD="rg -n --no-heading"; \ + else \ + MATCH_CMD="grep -REn"; \ + fi; \ + if eval "$$MATCH_CMD 'async\\s+[A-Za-z0-9]+(ApiV[0-9]+|Get|Post|Put|Patch|Delete)\\b' src/generated/sdk"; then \ echo "Found verbose generated method names. Update generate-method-names-overlay.py rules."; \ exit 1; \ fi diff --git a/sdks/typescript/src/generated/funcs/agents-init.ts b/sdks/typescript/src/generated/funcs/agents-init.ts index 1cb02298..508b00ed 100644 --- a/sdks/typescript/src/generated/funcs/agents-init.ts +++ b/sdks/typescript/src/generated/funcs/agents-init.ts @@ -45,7 +45,7 @@ import { Result } from "../types/fp.js"; * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) + * InitAgentResponse with created flag and active controls (policy-derived + direct) */ export function agentsInit( client: AgentControlSDKCore, diff --git a/sdks/typescript/src/generated/sdk/agents.ts b/sdks/typescript/src/generated/sdk/agents.ts index 6aee0bf0..d606b384 100644 --- a/sdks/typescript/src/generated/sdk/agents.ts +++ b/sdks/typescript/src/generated/sdk/agents.ts @@ -72,7 +72,7 @@ export class Agents extends ClientSDK { * db: Database session (injected) * * Returns: - * InitAgentResponse with created flag and active controls (if policy assigned) + * InitAgentResponse with created flag and active controls (policy-derived + direct) */ async init( request: models.InitAgentRequest, From 51abe3875206dd1152b23ff4deb15d72d8e5048d Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Mon, 2 Mar 2026 19:55:38 -0800 Subject: [PATCH 30/32] refactor(examples): remove policy-based association flows --- examples/README.md | 2 +- examples/agent_control_demo/demo_agent.py | 18 +-- examples/agent_control_demo/setup_controls.py | 147 ++++-------------- examples/crewai/README.md | 9 +- examples/crewai/content_agent_protection.py | 4 +- examples/crewai/setup_content_controls.py | 60 ++----- examples/customer_support_agent/README.md | 4 +- examples/customer_support_agent/run_demo.py | 22 +-- .../setup_demo_controls.py | 48 +----- .../customer_support_agent/support_agent.py | 4 +- examples/deepeval/README.md | 4 +- examples/deepeval/setup_controls.py | 57 +------ examples/galileo/README.md | 3 +- examples/langchain/README.md | 13 +- .../langchain/langgraph_auto_schema_agent.py | 2 +- examples/langchain/setup_sql_controls.py | 59 ++----- examples/langchain/sql_agent_protection.py | 2 +- examples/steer_action_demo/README.md | 2 +- .../autonomous_agent_demo.py | 18 +-- examples/steer_action_demo/setup_controls.py | 56 +------ 20 files changed, 116 insertions(+), 418 deletions(-) diff --git a/examples/README.md b/examples/README.md index 7d22e475..fbf1f37a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,7 +47,7 @@ uv run python examples/agent_control_demo/update_controls.py --block-ssn **Files:** - `setup_controls.py` - Create and configure controls via SDK -- `demo_agent.py` - Agent that uses `@control` decorator with server-side policies +- `demo_agent.py` - Agent that uses `@control` decorator with server-side controls - `update_controls.py` - Dynamically update controls without code changes - `agent_luna_demo.py` - Luna-2 evaluator integration for AI safety checks diff --git a/examples/agent_control_demo/demo_agent.py b/examples/agent_control_demo/demo_agent.py index 13b9a5a8..6e139698 100644 --- a/examples/agent_control_demo/demo_agent.py +++ b/examples/agent_control_demo/demo_agent.py @@ -77,12 +77,12 @@ def simulate_database_query(sql: str) -> str: # AGENT FUNCTIONS WITH CONTROLS # ============================================================================= -@control(policy="demo-policy") +@control() async def chat(message: str) -> str: """ - Chat function protected by the demo policy. - - The policy's 'block-ssn-output' control checks OUTPUT for SSN patterns. + Chat function protected by agent-associated controls. + + The 'block-ssn-output' control checks OUTPUT for SSN patterns. This prevents PII leakage in responses. """ print(f" [Agent] Processing message: {message}") @@ -91,12 +91,12 @@ async def chat(message: str) -> str: return response -@control(policy="demo-policy") +@control() async def execute_query(query: str) -> str: """ - Database query function protected by the demo policy. - - The policy's 'block-dangerous-sql' control checks INPUT for dangerous + Database query function protected by agent-associated controls. + + The 'block-dangerous-sql' control checks INPUT for dangerous SQL keywords and blocks before execution. """ print(f" [Agent] Executing query: {query}") @@ -110,7 +110,7 @@ async def process_request(input: str) -> str: """ General processing function with agent-associated controls applied. - Controls associated through policy or direct links are evaluated. + Controls directly associated to the agent are evaluated. """ print(f" [Agent] Processing request: {input}") response = simulate_llm_response(input) diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index c39a1bf4..839cc20d 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -5,13 +5,12 @@ This script demonstrates the full control lifecycle: 1. Create an agent 2. Create controls (regex and list) -3. Create a policy and add controls to it -4. Assign the policy to the agent +3. Associate controls directly with the agent 5. List controls 6. Update a control API Structure: - Agent → Policy → Controls + Agent → Controls Prerequisites: - Agent Control server running at http://localhost:8000 @@ -185,74 +184,27 @@ async def create_list_control(client: AgentControlClient) -> int: -async def create_policy(client: AgentControlClient, name: str) -> int: - """Create a policy.""" - print("\n" + "=" * 60) - print("STEP 4: Creating Policy") - print("=" * 60) - - try: - response = await client.http_client.put( - "/api/v1/policies", - json={"name": name} - ) - - if response.status_code == 409: - print(f" ℹ️ Policy '{name}' already exists") - return -1 - - response.raise_for_status() - policy_id = response.json().get("policy_id") - - print(f"✓ Created policy '{name}' with ID: {policy_id}") - return policy_id - - except Exception as e: - print(f"✗ Failed to create policy: {e}") - raise - - -async def add_control_to_policy( - client: AgentControlClient, - policy_id: int, - control_id: int -) -> bool: - """Add a control directly to a policy.""" - try: - response = await client.http_client.post( - f"/api/v1/policies/{policy_id}/controls/{control_id}" - ) - response.raise_for_status() - data = response.json() - print(f" ✓ Added control {control_id} to policy {policy_id}") - print(f" Response: {data}") - return True - except Exception as e: - print(f" ✗ Failed to add control to policy: {e}") - return False - - -async def assign_policy_to_agent( +async def add_control_to_agent( client: AgentControlClient, agent_name: str, - policy_id: int + control_id: int, ) -> bool: - """Assign a policy to an agent.""" + """Associate a control directly with an agent.""" print("\n" + "=" * 60) - print("STEP 5: Assigning Policy to Agent") + print("STEP 4: Associating Control to Agent") print("=" * 60) try: response = await client.http_client.post( - f"/api/v1/agents/{agent_name}/policies/{policy_id}" + f"/api/v1/agents/{agent_name}/controls/{control_id}" ) response.raise_for_status() data = response.json() - print(f"✓ Assigned policy {policy_id} to agent {agent_name}") + print(f"✓ Associated control {control_id} to agent {agent_name}") print(f" Response: {data}") return True except Exception as e: - print(f"✗ Failed to assign policy: {e}") + print(f"✗ Failed to associate control to agent: {e}") return False @@ -379,29 +331,8 @@ async def verify_full_chain(client: AgentControlClient, agent_name: str) -> None except Exception as e: print(f" Error: {e}") - # 2. Get agent policy associations - print("\n2. Agent Policy Associations:") - try: - resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/policies") - resp.raise_for_status() - policy_ids = resp.json().get("policy_ids", []) - policy_id = policy_ids[0] if policy_ids else None - print(f" Policy IDs: {policy_ids}") - - if policy_id is not None: - # 3. Get policy's controls - print("\n3. Policy's Controls:") - resp = await client.http_client.get(f"/api/v1/policies/{policy_id}/controls") - resp.raise_for_status() - ctrl_data = resp.json() - control_ids = ctrl_data.get("control_ids", []) - print(f" Control IDs: {control_ids}") - except Exception as e: - print(f" Error: {e}") - policy_id = None - - # 4. Final: List agent controls (the API we're testing) - print("\n4. Final Agent Controls (via /agents/{id}/controls):") + # 2. Final: List agent controls (the API we're testing) + print("\n2. Final Agent Controls (via /agents/{id}/controls):") try: resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/controls") resp.raise_for_status() @@ -456,53 +387,29 @@ async def main(): 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_name) - return - - # 4. Add controls to policy - print("\n Adding controls to policy...") - ok1 = await add_control_to_policy(client, policy_id, regex_control_id) - ok2 = await add_control_to_policy(client, policy_id, list_control_id) + # 3. Associate controls directly with the agent + print("\n Associating controls directly to agent...") + ok1 = await add_control_to_agent(client, agent_name, regex_control_id) + ok2 = await add_control_to_agent(client, agent_name, list_control_id) if not (ok1 and ok2): - print("\n⚠️ Failed to add controls to policy!") - - # Verify: List controls in policy - print("\n Verifying policy contents...") - try: - resp = await client.http_client.get( - f"/api/v1/policies/{policy_id}/controls" - ) - resp.raise_for_status() - policy_controls = resp.json() - print(f" Policy {policy_id} has controls: {policy_controls}") - except Exception as e: - print(f" Failed to verify policy: {e}") - - # 5. Assign policy to agent - ok3 = await assign_policy_to_agent(client, agent_name, policy_id) - if not ok3: - print("\n⚠️ Failed to assign policy to agent!") + print("\n⚠️ Failed to associate one or more controls to agent!") - # Verify: Get agent policy associations - print("\n Verifying agent policy assignment...") + # Verify: ensure both controls are active on the agent + print("\n Verifying direct control associations...") try: - resp = await client.http_client.get( - f"/api/v1/agents/{agent_name}/policies" - ) + resp = await client.http_client.get(f"/api/v1/agents/{agent_name}/controls") resp.raise_for_status() - assigned_policy_ids = resp.json().get("policy_ids", []) - if policy_id in assigned_policy_ids: - print(f" ✓ Agent correctly assigned to policy {policy_id}") + active_controls = resp.json().get("controls", []) + active_ids = {control.get("id") for control in active_controls} + expected_ids = {regex_control_id, list_control_id} + if expected_ids.issubset(active_ids): + print(f" ✓ Agent has expected control IDs: {sorted(expected_ids)}") else: print( - f" ⚠️ Agent policy IDs are {assigned_policy_ids}, expected to include {policy_id}" + f" ⚠️ Active control IDs are {sorted(active_ids)}, expected at least {sorted(expected_ids)}" ) except Exception as e: - print(f" ✗ Failed to verify agent policy: {e}") + print(f" ✗ Failed to verify direct control associations: {e}") # 6. List controls await list_agent_controls(client, agent_name) @@ -530,7 +437,7 @@ async def main(): - Keywords: DROP, DELETE, TRUNCATE, ALTER, GRANT, REVOKE, EXECUTE, SHUTDOWN, BACKUP API Flow: - Agent → Policy → Controls + Agent → Controls Now run the agent demo: uv run python examples/agent_control_demo/demo_agent.py diff --git a/examples/crewai/README.md b/examples/crewai/README.md index dea99856..72cd9114 100644 --- a/examples/crewai/README.md +++ b/examples/crewai/README.md @@ -109,8 +109,7 @@ This creates: - Unauthorized access control (blocks requests for other users' data - PRE-execution) - PII detection control for tool outputs (blocks SSN, credit cards, emails, phones - POST-execution) - Final output validation control (catches agent-generated PII - POST-execution) -- Policy with all three controls -- Assigns policy to the customer support crew agent +- Direct association of all three controls with the customer support crew agent ## Running the Example @@ -157,7 +156,7 @@ uv run content_agent_protection.py 🚫 [LAYER 2: Agent Control POST] BLOCKED Reason: Control 'pii-detection-output' matched Tool executed but output contained violations - LLM generated content that violated policies + LLM generated content that violated controls 🚫 SECURITY VIOLATION (POST-execution): [Details...] ``` @@ -390,7 +389,7 @@ This control catches PII in the final crew output, protecting against orchestrat Agent Control works seamlessly **with** CrewAI's agent orchestration: 1. **CrewAI Agent Layer**: Plans tasks, selects tools, manages conversation flow -2. **Agent Control Layer**: Enforces policies and business rules at tool boundaries +2. **Agent Control Layer**: Enforces controls and business rules at tool boundaries ``` User Request @@ -475,7 +474,7 @@ Return result or raise ControlViolationError ## Files - `content_agent_protection.py` - Main CrewAI crew with @control() -- `setup_content_controls.py` - One-time setup for controls/policies +- `setup_content_controls.py` - One-time setup for controls/direct associations - `pyproject.toml` - Dependencies - `README.md` - This file diff --git a/examples/crewai/content_agent_protection.py b/examples/crewai/content_agent_protection.py index 2827d4e3..a2d4a2f3 100644 --- a/examples/crewai/content_agent_protection.py +++ b/examples/crewai/content_agent_protection.py @@ -298,7 +298,7 @@ def handle_ticket_tool(ticket: str) -> str: print("\n🚫 [LAYER 2: Agent Control POST] BLOCKED") print(f" Reason: {e.message}") print(" Tool executed but output contained violations") - print(" LLM generated content that violated policies") + print(" LLM generated content that violated controls") stage = "POST-execution" error_msg = f"🚫 SECURITY VIOLATION ({stage}): {e.message}\n\nThis request has been logged for security review." @@ -377,7 +377,7 @@ def create_support_crew(): goal="Provide helpful customer support while protecting user privacy and data security", backstory=( "You are an experienced customer support agent who helps customers with their questions. " - "You are friendly, professional, and always respect customer privacy and data security policies." + "You are friendly, professional, and always respect customer privacy and data security controls." ), tools=[ticket_handler_tool], verbose=True diff --git a/examples/crewai/setup_content_controls.py b/examples/crewai/setup_content_controls.py index 779ae814..77501310 100644 --- a/examples/crewai/setup_content_controls.py +++ b/examples/crewai/setup_content_controls.py @@ -11,14 +11,14 @@ import asyncio import os -from agent_control import Agent, AgentControlClient, agents, controls, policies +from agent_control import Agent, AgentControlClient, agents, controls AGENT_ID = "989d84f0-9afe-4fb2-9e9e-e9d076271e29" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") async def setup_content_controls(): - """Create PII protection and unauthorized access controls, policy, and assign to agent.""" + """Create PII protection/unauthorized-access controls and add them directly to the agent.""" async with AgentControlClient(base_url=SERVER_URL) as client: # 1. Register Agent agent_name = AGENT_ID @@ -172,65 +172,35 @@ async def setup_content_controls(): else: raise - # 6. Create Policy + # 6. Associate controls directly with agent try: - policy_result = await policies.create_policy( - client, name="support-pii-protection-policy" - ) - policy_id = policy_result["policy_id"] - print(f"✓ Policy created (ID: {policy_id})") - except Exception as e: - if "409" in str(e): - print(f"⚠️ Policy 'support-pii-protection-policy' already exists.") - print(" Cannot proceed - SDK doesn't support looking up policies by name.") - print("\n To fix this, run one of these commands:") - print(" 1. Delete via server API:") - print(f" curl -X DELETE {SERVER_URL}/api/v1/policies/") - print(" 2. Or use the server UI to delete the policy") - print("\n Then re-run this script.") - raise SystemExit(1) - raise - - # 7. Add Controls to Policy - try: - await policies.add_control_to_policy(client, policy_id, unauthorized_control_id) - print(f"✓ Added unauthorized access control to policy") - except Exception as e: - if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ Unauthorized access control already in policy (OK)") - else: - print(f"❌ Failed to add control to policy: {e}") - raise - - try: - await policies.add_control_to_policy(client, policy_id, pii_control_id) - print(f"✓ Added PII detection control to policy") + await agents.add_agent_control(client, agent_name, unauthorized_control_id) + print("✓ Added unauthorized access control to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ PII detection control already in policy (OK)") + print("ℹ️ Unauthorized access control already associated with agent (OK)") else: - print(f"❌ Failed to add control to policy: {e}") + print(f"❌ Failed to add unauthorized access control to agent: {e}") raise try: - await policies.add_control_to_policy(client, policy_id, final_output_control_id) - print(f"✓ Added final output validation control to policy") + await agents.add_agent_control(client, agent_name, pii_control_id) + print("✓ Added PII detection control to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ Final output validation control already in policy (OK)") + print("ℹ️ PII detection control already associated with agent (OK)") else: - print(f"❌ Failed to add control to policy: {e}") + print(f"❌ Failed to add PII detection control to agent: {e}") raise - # 8. Assign Policy to Agent try: - await policies.assign_policy_to_agent(client, agent_name, policy_id) - print(f"✓ Assigned policy to agent") + await agents.add_agent_control(client, agent_name, final_output_control_id) + print("✓ Added final output validation control to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ Policy already assigned to agent (OK)") + print("ℹ️ Final output validation control already associated with agent (OK)") else: - print(f"❌ Failed to assign policy: {e}") + print(f"❌ Failed to add final output validation control to agent: {e}") raise print("\n✅ Setup complete! You can now run content_agent_protection.py") diff --git a/examples/customer_support_agent/README.md b/examples/customer_support_agent/README.md index bd2d843a..3d3adf1b 100644 --- a/examples/customer_support_agent/README.md +++ b/examples/customer_support_agent/README.md @@ -208,10 +208,10 @@ except ControlViolationError as e: **Important**: Controls are defined on the server via the UI, not in code. This design provides: -- **Centralized management**: Security team controls policies without code changes +- **Centralized management**: Security team controls safeguards without code changes - **Instant updates**: Change controls without redeploying agents - **Audit trail**: Server logs all control evaluations -- **Separation of concerns**: Developers focus on features, security team on policies +- **Separation of concerns**: Developers focus on features, security team on safeguards ## Project Structure diff --git a/examples/customer_support_agent/run_demo.py b/examples/customer_support_agent/run_demo.py index 773d1119..5b612233 100644 --- a/examples/customer_support_agent/run_demo.py +++ b/examples/customer_support_agent/run_demo.py @@ -7,7 +7,7 @@ Usage: python run_demo.py # Interactive chat mode (default) python run_demo.py --automated # Run automated test scenarios - python run_demo.py --reset # Reset agent (remove policy/controls) and exit + python run_demo.py --reset # Reset agent (remove control associations) and exit Test Commands (in interactive mode): /test-safe Run safe message tests @@ -59,7 +59,7 @@ async def reset_agent(): - """Reset the agent by removing policy and direct control associations.""" + """Reset the agent by removing direct control associations.""" agent_name = AGENT_ID server_url = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -81,20 +81,6 @@ async def reset_agent(): print(f"Error checking agent: {e}") return - # Remove all policies from agent (disconnects policy-derived controls) - try: - await agents.remove_all_agent_policies(client, agent_name) - logger.info("Successfully removed all policies from agent") - print("Removed all policies from agent.") - except Exception as e: - if "404" in str(e): - logger.info("Agent not found while removing policies") - print("Agent not found - nothing to reset.") - return - logger.error(f"Error removing policies: {e}") - print(f"Error removing policies: {e}") - return - # Remove direct control associations (idempotent per control ID). removed_direct_associations = 0 cursor: int | None = None @@ -135,7 +121,7 @@ async def reset_agent(): ) print() - print("Reset complete. The agent now has no policy or direct control associations.") + print("Reset complete. The agent now has no direct control associations.") print("Run the demo again and add controls via the UI to test.") @@ -614,7 +600,7 @@ def main(): parser.add_argument( "--reset", action="store_true", - help="Reset the agent (remove policy/controls) and exit" + help="Reset the agent (remove control associations) and exit" ) args = parser.parse_args() diff --git a/examples/customer_support_agent/setup_demo_controls.py b/examples/customer_support_agent/setup_demo_controls.py index 01746e68..7b7b8a3c 100644 --- a/examples/customer_support_agent/setup_demo_controls.py +++ b/examples/customer_support_agent/setup_demo_controls.py @@ -5,15 +5,14 @@ This script: 1. Registers the agent with the server 2. Creates demo controls (PII detection, prompt injection) -3. Creates a policy and attaches controls -4. Assigns the policy to the agent +3. Directly associates controls to the agent Run this after starting the server to have a working demo out of the box. """ import asyncio import os -from agent_control import Agent, AgentControlClient, agents, controls, policies +from agent_control import Agent, AgentControlClient, agents, controls # Same agent ID as in support_agent.py AGENT_ID = "646d5dea-c2e6-4453-b446-7035482b38e4" @@ -252,42 +251,7 @@ async def setup_demo(quiet: bool = False): print(f" Error registering agent: {e}") return False - # Get or create a policy for the agent - policy_name = f"policy-{AGENT_ID}" - policy_id = None - - # Check if agent already has policies - try: - policy_info = await agents.get_agent_policies(client, agent_name) - policy_ids = policy_info.get("policy_ids", []) - policy_id = policy_ids[0] if policy_ids else None - except Exception: - policy_id = None # No policy associations yet - - # Create policy if needed - if not policy_id: - try: - policy_result = await policies.create_policy(client, policy_name) - policy_id = policy_result["policy_id"] - print(f" Created policy: {policy_name}") - except Exception as e: - if "409" in str(e): - import time - policy_name = f"policy-{AGENT_ID}-{int(time.time())}" - policy_result = await policies.create_policy(client, policy_name) - policy_id = policy_result["policy_id"] - print(f" Created policy: {policy_name}") - else: - print(f" Error setting up policy: {e}") - return False - - try: - await policies.assign_policy_to_agent(client, agent_name, policy_id) - except Exception as e: - print(f" Error assigning policy: {e}") - return False - - # Create controls and add to policy + # Create controls and directly associate them to the agent controls_created = 0 for control_spec in DEMO_CONTROLS: control_name = control_spec["name"] @@ -313,9 +277,11 @@ async def setup_demo(quiet: bool = False): continue try: - await policies.add_control_to_policy(client, policy_id, control_id) + await agents.add_agent_control(client, agent_name, control_id) except Exception as e: - print(f" Error adding control '{control_name}' to policy: {e}") + if "409" in str(e) or "already" in str(e).lower(): + continue + print(f" Error adding control '{control_name}' to agent: {e}") continue if controls_created > 0: diff --git a/examples/customer_support_agent/support_agent.py b/examples/customer_support_agent/support_agent.py index 172cf559..aa6fb56d 100644 --- a/examples/customer_support_agent/support_agent.py +++ b/examples/customer_support_agent/support_agent.py @@ -10,7 +10,7 @@ 4. Realistic enterprise patterns (mock services, multiple tools) NOTE: Controls are defined on the server via the UI, not in code. -This keeps security policies centrally managed and separate from code. +This keeps security controls centrally managed and separate from code. """ import asyncio @@ -49,7 +49,7 @@ class MockLLM: RESPONSES = { "greeting": "Hello! I'm your customer support assistant. How can I help you today?", "refund": "I understand you'd like a refund. Let me look into your order. " - "Our refund policy allows returns within 30 days of purchase.", + "Our refund guidelines allow returns within 30 days of purchase.", "technical": "I can help with technical issues. Could you describe the problem " "you're experiencing in more detail?", "status": "I'll check the status of your order right away. " diff --git a/examples/deepeval/README.md b/examples/deepeval/README.md index e98e8ef4..12768a8c 100644 --- a/examples/deepeval/README.md +++ b/examples/deepeval/README.md @@ -380,7 +380,7 @@ python -m build # Creates dist/*.whl - Deploy your own agent-control server instance - Install custom evaluator packages (wheel, source, or private PyPI) - Your agents connect to this server via the SDK - - Complete control over evaluators and policies + - Complete control over evaluators and controls 2. **Managed Service (If Available)** - Use a hosted agent-control service @@ -455,7 +455,7 @@ You can create specialized evaluators for specific use cases: 2. **Extensibility**: The `Evaluator` base class makes it easy to integrate any evaluation library 3. **Configuration**: Pydantic models provide type-safe, validated configuration 4. **Registration**: The `@register_evaluator` decorator handles registration automatically -5. **Integration**: Evaluators work seamlessly with agent-control's policy system +5. **Integration**: Evaluators work seamlessly with agent-control's control system 6. **Control Logic**: `matched=True` triggers the action (deny/allow), so invert when quality passes ## Troubleshooting diff --git a/examples/deepeval/setup_controls.py b/examples/deepeval/setup_controls.py index 97bf5dd2..cbf2eda1 100755 --- a/examples/deepeval/setup_controls.py +++ b/examples/deepeval/setup_controls.py @@ -5,8 +5,7 @@ This script: 1. Registers the agent with the server 2. Creates DeepEval GEval evaluator controls for quality checks -3. Creates a policy and attaches controls -4. Assigns the policy to the agent +3. Directly associates controls to the agent The controls demonstrate using DeepEval's LLM-as-a-judge to enforce: - Response coherence @@ -187,52 +186,7 @@ async def setup_demo(quiet: bool = False): print(f"❌ Error registering agent: {e}") return False - # Get or create a policy for the agent - policy_name = f"policy-{AGENT_ID}" - policy_id = None - - # Check if agent already has associated policies - try: - resp = await client.get(f"/api/v1/agents/{agent_name}/policies") - if resp.status_code == 200: - policy_ids = resp.json().get("policy_ids", []) - if policy_ids: - policy_id = policy_ids[0] - print(f"✓ Found existing policy: {policy_id}") - except httpx.HTTPError: - pass # No policies yet - - # Create policy if needed - if not policy_id: - try: - resp = await client.put( - "/api/v1/policies", - json={"name": policy_name}, - ) - if resp.status_code == 409: - # Policy name exists but not assigned - create with unique name - import time - - policy_name = f"policy-{AGENT_ID}-{int(time.time())}" - resp = await client.put( - "/api/v1/policies", - json={"name": policy_name}, - ) - resp.raise_for_status() - policy_id = resp.json()["policy_id"] - print(f"✓ Created policy: {policy_name}") - - # Associate policy with agent - resp = await client.post( - f"/api/v1/agents/{agent_name}/policies/{policy_id}" - ) - resp.raise_for_status() - print("✓ Associated policy with agent") - except httpx.HTTPError as e: - print(f"❌ Error setting up policy: {e}") - return False - - # Create controls and add to policy + # Create controls and associate them directly with the agent print() print("Creating DeepEval controls...") controls_created = 0 @@ -271,9 +225,10 @@ async def setup_demo(quiet: bool = False): ) resp.raise_for_status() - # Add control to policy - resp = await client.post(f"/api/v1/policies/{policy_id}/controls/{control_id}") - resp.raise_for_status() + # Associate control directly with the agent + resp = await client.post(f"/api/v1/agents/{agent_name}/controls/{control_id}") + if resp.status_code not in (200, 409): + resp.raise_for_status() status = "✓" if definition.get("enabled") else "○" enabled_text = "enabled" if definition.get("enabled") else "disabled" diff --git a/examples/galileo/README.md b/examples/galileo/README.md index 96e141d0..b87170db 100644 --- a/examples/galileo/README.md +++ b/examples/galileo/README.md @@ -41,7 +41,7 @@ The demo tests various inputs against a pre-configured toxicity detection stage: ### Central vs Local Stages -- **Central Stage** (used in this demo): Rulesets and policies are pre-configured on the Galileo server. Simply reference the stage by name. +- **Central Stage** (used in this demo): Rulesets are pre-configured on the Galileo server. Simply reference the stage by name. - **Local Stage**: Define rulesets at runtime in your code (see evaluator documentation). ### Expected Output @@ -85,4 +85,3 @@ Testing toxicity detection with Central Stage... - [Galileo Protect Overview](https://v2docs.galileo.ai/concepts/protect/overview) - [Luna-2 Python API Reference](https://v2docs.galileo.ai/sdk-api/python/reference/protect) - [Agent Control Luna-2 Evaluator](../../evaluators/extra/galileo/) - diff --git a/examples/langchain/README.md b/examples/langchain/README.md index 82a87e55..56db9793 100644 --- a/examples/langchain/README.md +++ b/examples/langchain/README.md @@ -41,8 +41,7 @@ uv run setup_sql_controls.py This creates: - SQL safety control (blocks DROP, DELETE, TRUNCATE, ALTER, GRANT) -- Policy with the control -- Associates policy with the SQL agent +- Direct association of the control with the SQL agent > For local execution, create the control with `execution: "sdk"` in > `setup_sql_controls.py` (see `sql_control_data_sdk`) and enable @@ -145,17 +144,17 @@ pkill -f "uvicorn agent_control_server" cd server && make run ``` -### "Policy 'sql-protection-policy' already exists" +### "Control already associated with agent" -**Cause:** Setup script was run multiple times. +**Cause:** Setup script was run multiple times and the direct association already exists. -**Fix:** Either delete the policy via the API or use a different name in `setup_sql_controls.py`. +**Fix:** This is expected/idempotent; you can ignore and continue. ### DROP TABLE still executes **Causes:** 1. Server not running or evaluators not loaded (remote mode) -2. Control not associated with the agent (policy or direct) +2. Control not associated directly with the agent 3. Control data missing/invalid (control not returned to agent) 4. Local mode enabled but control is still `execution: "server"` @@ -192,7 +191,7 @@ Raise ControlViolationError (DENY) or Execute (ALLOW) ## Files - `sql_agent_protection.py` - Main SQL agent with `@control()` decorator -- `setup_sql_controls.py` - One-time setup script for controls/policies +- `setup_sql_controls.py` - One-time setup script for controls/direct associations - `pyproject.toml` - Dependencies and configuration - `README.md` - This file diff --git a/examples/langchain/langgraph_auto_schema_agent.py b/examples/langchain/langgraph_auto_schema_agent.py index b077d0df..b6afb26a 100644 --- a/examples/langchain/langgraph_auto_schema_agent.py +++ b/examples/langchain/langgraph_auto_schema_agent.py @@ -264,7 +264,7 @@ async def main() -> None: final_message = result["messages"][-1] print(f"Assistant: {final_message.content}") except ControlViolationError as exc: - print(f"Assistant: Request blocked by control policy: {exc.message}") + print(f"Assistant: Request blocked by control rules: {exc.message}") except RuntimeError as exc: print( "Assistant: Control evaluation is unavailable. " diff --git a/examples/langchain/setup_sql_controls.py b/examples/langchain/setup_sql_controls.py index 7feffd33..88ad4fbe 100644 --- a/examples/langchain/setup_sql_controls.py +++ b/examples/langchain/setup_sql_controls.py @@ -1,7 +1,7 @@ """ Setup script for SQL agent controls. -This script creates the SQL control and policy on the server. +This script creates the SQL control and associates it directly to the agent. Run this once before running sql_agent_protection.py. NOTE: This script is designed to run once. If resources already exist, @@ -16,7 +16,7 @@ import requests -from agent_control import Agent, AgentControlClient, agents, controls, policies +from agent_control import Agent, AgentControlClient, agents, controls AGENT_ID = "edf66504-0db5-4ee8-9e09-3ef37bbb8faa" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -48,7 +48,7 @@ def setup_database(): async def setup_sql_controls(): - """Create SQL control, policy, and assign to agent.""" + """Create SQL control and associate it directly with the agent.""" async with AgentControlClient(base_url=SERVER_URL) as client: # 1. Register Agent agent_name = AGENT_ID @@ -128,56 +128,15 @@ async def setup_sql_controls(): await controls.set_control_data(client, control_id, sql_control_data) print("✓ Control configuration updated") - # 3. Create Policy + # 3. Associate control directly with agent try: - policy_result = await policies.create_policy( - client, name="sql-protection-policy" - ) - policy_id = policy_result["policy_id"] - print(f"✓ Policy created (ID: {policy_id})") - except Exception as e: - if "409" in str(e): - print("ℹ️ Policy 'sql-protection-policy' already exists, checking agent...") - try: - policy_info = await agents.get_agent_policies(client, str(agent_name)) - policy_ids = policy_info.get("policy_ids", []) - policy_id = policy_ids[0] if policy_ids else None - if policy_id is None: - raise ValueError("No policies assigned to agent.") - print(f"ℹ️ Using agent's existing policy (ID: {policy_id})") - except Exception: - # Create a new policy name to avoid conflicts and keep script idempotent. - unique_name = f"sql-protection-policy-{uuid.uuid4().hex[:8]}" - print("⚠️ Policy exists but could not resolve its ID.") - print(f" Creating a new policy '{unique_name}' instead.") - policy_result = await policies.create_policy( - client, name=unique_name - ) - policy_id = policy_result["policy_id"] - print(f"✓ Policy created (ID: {policy_id})") - else: - raise - - # 4. Add Control to Policy - try: - await policies.add_control_to_policy(client, policy_id, control_id) - print(f"✓ Added control to policy") - except Exception as e: - if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ Control already in policy (OK)") - else: - print(f"❌ Failed to add control to policy: {e}") - raise - - # 5. Assign Policy to Agent - try: - await policies.assign_policy_to_agent(client, agent_name, policy_id) - print(f"✓ Assigned policy to agent") + await agents.add_agent_control(client, agent_name, control_id) + print("✓ Associated control directly with agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): - print(f"ℹ️ Policy already assigned to agent (OK)") + print("ℹ️ Control already associated with agent (OK)") else: - print(f"❌ Failed to assign policy: {e}") + print(f"❌ Failed to associate control with agent: {e}") raise print("\n✅ Setup complete! You can now run sql_agent_protection.py") @@ -193,5 +152,5 @@ async def setup_sql_controls(): setup_database() print() - # Step 2: Setup controls and policies + # Step 2: Setup controls and direct agent associations asyncio.run(setup_sql_controls()) diff --git a/examples/langchain/sql_agent_protection.py b/examples/langchain/sql_agent_protection.py index b0fd84f6..eb051085 100644 --- a/examples/langchain/sql_agent_protection.py +++ b/examples/langchain/sql_agent_protection.py @@ -5,7 +5,7 @@ with graceful error handling. PREREQUISITE: - Run setup_sql_controls.py FIRST to create the SQL control and policy: + Run setup_sql_controls.py FIRST to create the SQL control and direct agent association: $ uv run setup_sql_controls.py diff --git a/examples/steer_action_demo/README.md b/examples/steer_action_demo/README.md index dca3b531..73abdf6e 100644 --- a/examples/steer_action_demo/README.md +++ b/examples/steer_action_demo/README.md @@ -13,7 +13,7 @@ This example shows all three AgentControl action types in a real-world banking s ## Understanding Steer Actions -**Steer is a non-fatal control signal** - unlike DENY which blocks execution, STEER provides corrective guidance to help agents satisfy policy requirements: +**Steer is a non-fatal control signal** - unlike DENY which blocks execution, STEER provides corrective guidance to help agents satisfy control requirements: - **Philosophy**: Agents are expected to correct the issue and retry - **Behavior**: Raises `ControlSteerError` with structured guidance diff --git a/examples/steer_action_demo/autonomous_agent_demo.py b/examples/steer_action_demo/autonomous_agent_demo.py index 20bfd7db..a2078f68 100644 --- a/examples/steer_action_demo/autonomous_agent_demo.py +++ b/examples/steer_action_demo/autonomous_agent_demo.py @@ -134,7 +134,7 @@ def check_fraud_score(amount: float, destination: str) -> float: """Check fraud risk based on destination. Note: Amount thresholds are handled by steer controls, not here. - Controls are the source of truth for policy decisions. + Controls are the source of truth for transfer decisions. """ agent_think("Running fraud detection...") if "north korea" in destination.lower() or "iran" in destination.lower(): @@ -311,7 +311,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: else: fraud_score = state["fraud_score"] - agent_think("Checking compliance and policy controls...") + agent_think("Checking compliance controls...") log_trace("agent-control", "Initiating pre-execution control evaluation") agent_reason("All transfers must pass control checks before execution") @@ -341,7 +341,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: # Success! log_trace("agent-control", "All control checks passed ✓") log_trace("execution", "Wire transfer executed successfully") - agent_reason("Transfer complies with all policies - proceeding with execution") + agent_reason("Transfer complies with all controls - proceeding with execution") # Show if this was a successful retry after steer correction if state.get("verified_2fa") or state.get("manager_approved"): @@ -372,8 +372,8 @@ async def process_transfer_node(state: AgentState) -> AgentState: except ControlViolationError as e: # DENY - Hard block log_trace("agent-control", f"DENY control triggered: {e.control_name}") - log_trace("compliance", "Transaction blocked by compliance policy") - agent_reason("This transaction violates a hard policy rule - cannot proceed") + log_trace("compliance", "Transaction blocked by compliance control") + agent_reason("This transaction violates a hard control rule - cannot proceed") agent_say(f"❌ I cannot process this transfer.") # Control Evaluation Summary @@ -393,7 +393,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: print(f" \033[90m{e.metadata}\033[0m") log_trace("audit", f"Blocked transaction: Control={e.control_name}, Amount=${request['amount']:,.2f}") - agent_say("This transaction violates compliance policies and cannot be approved under any circumstances.") + agent_say("This transaction violates compliance controls and cannot be approved under any circumstances.") return { **state, @@ -407,7 +407,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: log_trace("control", f"Steer action triggered by control '{e.control_name}'") log_trace("decision", "Transfer cannot proceed without additional approvals") - agent_reason("Control policy requires verification before large transactions") + agent_reason("Control rules require verification before large transactions") # Parse structured steering context (deterministic, no LLM needed) agent_think("Parsing steering context...") @@ -492,7 +492,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: elif action == "approval" and not state.get("manager_approved"): log_escalation("ESCALATING TO: Manager Approval System") agent_reason(f"Control requires manager approval for this ${request['amount']:,.2f} transfer") - log_trace("compliance", "Manager approval required per control policy") + log_trace("compliance", "Manager approval required per control rules") # Get justification if not state.get("justification"): @@ -544,7 +544,7 @@ async def process_transfer_node(state: AgentState) -> AgentState: if approval.lower() in ['yes', 'y']: log_trace("approval-system", "Manager approved the transfer") log_trace("audit", f"Approval logged: Manager ID: MGR-001, Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}") - agent_reason("Manager authorization received - transfer now complies with policy") + agent_reason("Manager authorization received - transfer now complies with controls") agent_say("✅ Manager approved!") print(f"\n 🔄 \033[1mSTEER CORRECTION COMPLETE - RETRYING TRANSFER\033[0m") diff --git a/examples/steer_action_demo/setup_controls.py b/examples/steer_action_demo/setup_controls.py index 3d4d2781..f6d8a0a2 100644 --- a/examples/steer_action_demo/setup_controls.py +++ b/examples/steer_action_demo/setup_controls.py @@ -2,7 +2,7 @@ Setup script for Banking Transaction Agent controls. Demonstrates all AgentControl action types in a realistic banking scenario: -- ALLOW: Simple transfers within policy +- ALLOW: Simple transfers that satisfy controls - DENY: Compliance violations (sanctioned countries, fraud) - STEER: Large transfers requiring verification and approval @@ -12,7 +12,7 @@ import asyncio import os -from agent_control import Agent, AgentControlClient, agents, controls, policies +from agent_control import Agent, AgentControlClient, agents, controls AGENT_ID = "f8e5d3c2-4b1a-4e7f-9c8d-2a3b4c5d6e7f" SERVER_URL = os.getenv("AGENT_CONTROL_URL", "http://localhost:8000") @@ -238,60 +238,18 @@ async def setup_banking_controls(): print(f" Response: {e.response.text if hasattr(e.response, 'text') else e.response}") raise - # 3. Create Policy - print("\n📋 Creating policy...") - try: - policy_result = await policies.create_policy( - client, - name="banking-transaction-policy" - ) - policy_id = policy_result["policy_id"] - print(f" ✓ Created policy: banking-transaction-policy (ID: {policy_id})") - except Exception as e: - if "409" in str(e): - print(f" ℹ️ Policy 'banking-transaction-policy' already exists") - try: - policy_info = await agents.get_agent_policies(client, agent.agent_name) - policy_ids = policy_info.get("policy_ids", []) - policy_id = policy_ids[0] if policy_ids else None - if policy_id is not None: - print(f" ℹ️ Using agent's existing policy (ID: {policy_id})") - else: - raise ValueError("No policy assigned") - except Exception: - import uuid as uuid_module - unique_name = f"banking-policy-{uuid_module.uuid4().hex[:8]}" - print(f" ⚠️ Creating new policy '{unique_name}' instead") - policy_result = await policies.create_policy(client, name=unique_name) - policy_id = policy_result["policy_id"] - print(f" ✓ Created policy (ID: {policy_id})") - else: - print(f" ❌ Error creating policy: {e}") - raise - - # 4. Add Controls to Policy - print("\n📋 Adding controls to policy...") + # 3. Associate controls directly with the agent + print("\n📋 Associating controls directly with agent...") for control_id in control_ids: try: - await policies.add_control_to_policy(client, policy_id, control_id) - print(f" ✓ Added control {control_id} to policy") + await agents.add_agent_control(client, agent.agent_name, control_id) + print(f" ✓ Added control {control_id} to agent") except Exception as e: if "409" in str(e) or "already" in str(e).lower(): - print(f" ℹ️ Control {control_id} already in policy (OK)") + print(f" ℹ️ Control {control_id} already associated with agent (OK)") else: print(f" ⚠️ Failed to add control: {e}") - # 5. Assign Policy to Agent - print("\n📋 Assigning policy to agent...") - try: - await policies.assign_policy_to_agent(client, agent.agent_name, policy_id) - print(f" ✓ Assigned policy {policy_id} to agent {agent.agent_name}") - except Exception as e: - if "409" in str(e) or "already" in str(e).lower(): - print(f" ℹ️ Policy already assigned to agent (OK)") - else: - print(f" ⚠️ Failed to assign policy: {e}") - print("\n✅ Setup complete!") print(f"\nAgent ID: {AGENT_ID}") print(f"Server URL: {SERVER_URL}") From da28bb89586eb5c6a5b21176f9c579499fe77017 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Tue, 3 Mar 2026 13:34:56 -0800 Subject: [PATCH 31/32] fix: address latest PR review threads --- README.md | 25 ++++++------------- examples/agent_control_demo/setup_controls.py | 5 ++-- sdks/python/src/agent_control/policies.py | 16 ++++++++---- sdks/python/tests/test_agent_id_validation.py | 2 +- .../query-hooks/use-add-control-to-agent.ts | 4 ++- .../controls/use-delete-control-flow.tsx | 2 ++ .../modals/control-store/index.tsx | 11 ++++++-- 7 files changed, 35 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 4268c144..ccc6b5b5 100644 --- a/README.md +++ b/README.md @@ -159,12 +159,12 @@ Create controls to protect your agent's operations: # setup.py - Run once to configure everything import asyncio from datetime import datetime, UTC -from agent_control import AgentControlClient, controls, policies, agents +from agent_control import AgentControlClient, controls, agents from agent_control_models import Agent async def setup(): async with AgentControlClient() as client: # Defaults to localhost:8000 - # 1. Register agent first (required before assigning policy) + # 1. Register agent first agent = Agent( # Your agent's UUID agent_name="550e8400-e29b-41d4-a716-446655440000", @@ -189,26 +189,15 @@ async def setup(): "action": {"decision": "deny"} } ) - # 3. Create policy - policy = await policies.create_policy(client, name="production-policy") - - # 4. Add control to policy - await policies.add_control_to_policy( - client, - policy_id=policy["policy_id"], - control_id=control["control_id"] - ) - - # 5. Assign policy to agent - await policies.assign_policy_to_agent( + # 3. Associate control directly with agent + await agents.add_agent_control( client, - agent_name=AGENT_ID, - policy_id=policy["policy_id"] + agent_name=agent.agent_name, + control_id=control["control_id"], ) print("✅ Setup complete!") print(f" Control ID: {control['control_id']}") - print(f" Policy ID: {policy['policy_id']}") asyncio.run(setup()) ``` @@ -235,7 +224,7 @@ from agent_control import control, ControlViolationError # Initialize your agent agent_control.init( - agent_name="550e8400-e29b-41d4-a716-446655440000", + agent_name="550e8400-e29b-41d4-a716-446655440000", # Agent identifier (UUID recommended) agent_description="My Chatbot", ) diff --git a/examples/agent_control_demo/setup_controls.py b/examples/agent_control_demo/setup_controls.py index 839cc20d..7bf9b9fd 100644 --- a/examples/agent_control_demo/setup_controls.py +++ b/examples/agent_control_demo/setup_controls.py @@ -6,8 +6,8 @@ 1. Create an agent 2. Create controls (regex and list) 3. Associate controls directly with the agent -5. List controls -6. Update a control +4. List controls +5. Update a control API Structure: Agent → Controls @@ -52,7 +52,6 @@ async def create_agent(client: AgentControlClient) -> str: json={ "agent": { "agent_name": str(agent_name), - "agent_name": AGENT_NAME, "agent_description": "Demo chatbot for testing controls", }, "steps": [] diff --git a/sdks/python/src/agent_control/policies.py b/sdks/python/src/agent_control/policies.py index cd9a6323..99420b19 100644 --- a/sdks/python/src/agent_control/policies.py +++ b/sdks/python/src/agent_control/policies.py @@ -2,8 +2,8 @@ from typing import Any, cast -from . import agents from .client import AgentControlClient +from .validation import ensure_agent_name async def create_policy( @@ -155,9 +155,11 @@ async def assign_policy_to_agent( policy_id: int ) -> dict[str, Any]: """ - Associate a policy with an agent. + Assign a single policy to an agent via compatibility endpoint. - This operation is additive and idempotent. Agents can have multiple policy associations. + This call uses replace semantics for backward compatibility: + existing policy associations are removed and replaced with ``policy_id``. + For additive behavior, use ``agents.add_agent_policy(...)``. Args: client: AgentControlClient instance @@ -171,5 +173,9 @@ async def assign_policy_to_agent( httpx.HTTPError: If request fails HTTPException 404: Agent or policy not found """ - # Keep policy module API while delegating to the canonical agents helper. - return await agents.add_agent_policy(client, agent_name, policy_id) + normalized_name = ensure_agent_name(agent_name) + response = await client.http_client.post( + f"/api/v1/agents/{normalized_name}/policy/{policy_id}" + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) diff --git a/sdks/python/tests/test_agent_id_validation.py b/sdks/python/tests/test_agent_id_validation.py index 6c5f0c2f..37fdc265 100644 --- a/sdks/python/tests/test_agent_id_validation.py +++ b/sdks/python/tests/test_agent_id_validation.py @@ -261,4 +261,4 @@ async def test_assign_policy_normalizes_agent_name() -> None: await policies.assign_policy_to_agent(client, "Agent-Example_01", policy_id=11) - client.http_client.post.assert_awaited_once_with("/api/v1/agents/agent-example_01/policies/11") + client.http_client.post.assert_awaited_once_with("/api/v1/agents/agent-example_01/policy/11") diff --git a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts index 98eaecd3..40c9ce2c 100644 --- a/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts +++ b/ui/src/core/hooks/query-hooks/use-add-control-to-agent.ts @@ -87,11 +87,13 @@ export function useAddControlToAgent() { }, onSuccess: (_data, variables) => { // Invalidate relevant queries to refetch data - queryClient.invalidateQueries({ queryKey: ['controls'] }); queryClient.invalidateQueries({ queryKey: ['agent', variables.agentId] }); queryClient.invalidateQueries({ queryKey: ['agent', variables.agentId, 'controls'], }); + queryClient.invalidateQueries({ + queryKey: ['controls', 'infinite'], + }); // Invalidate agents list query to refresh active controls count queryClient.invalidateQueries({ queryKey: ['agents', 'infinite'], diff --git a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx index 0c6929bc..5dcfef73 100644 --- a/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx +++ b/ui/src/core/page-components/agent-detail/controls/use-delete-control-flow.tsx @@ -46,6 +46,8 @@ export function useDeleteControlFlow({ }, { onSuccess: (result: RemoveControlFromAgentResult) => { + // The controls table shows active controls (direct + policy-derived), so + // remove-direct can legitimately no-op for policy-derived entries. if (!result.removed_direct_association) { notifications.show({ title: 'No direct association found', 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 1aae74b9..b6b6a79d 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 @@ -1,4 +1,5 @@ import { + Anchor, Box, Divider, Group, @@ -15,6 +16,7 @@ import { notifications } from '@mantine/notifications'; import { Button, Table } from '@rungalileo/jupiter-ds'; import { IconAlertCircle, IconX } from '@tabler/icons-react'; import { type ColumnDef } from '@tanstack/react-table'; +import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { ErrorBoundary } from '@/components/error-boundary'; @@ -276,9 +278,14 @@ export function ControlStoreModal({ ); } return ( - + {usedByAgent.agent_name} - + ); }, }, From f2197055b8abd648447cf8a69971629712f79ce5 Mon Sep 17 00:00:00 2001 From: Lev Neiman Date: Tue, 3 Mar 2026 14:41:14 -0800 Subject: [PATCH 32/32] fix: resolve ui and sdk-ts-ci CI failures --- sdks/typescript/src/generated/hooks/hooks.ts | 3 --- .../typescript/src/generated/hooks/registration.ts | 14 -------------- ui/tests/control-store.spec.ts | 13 +++++++++---- 3 files changed, 9 insertions(+), 21 deletions(-) delete mode 100644 sdks/typescript/src/generated/hooks/registration.ts diff --git a/sdks/typescript/src/generated/hooks/hooks.ts b/sdks/typescript/src/generated/hooks/hooks.ts index 7ed9e044..94ad0a32 100644 --- a/sdks/typescript/src/generated/hooks/hooks.ts +++ b/sdks/typescript/src/generated/hooks/hooks.ts @@ -18,8 +18,6 @@ import { SDKInitHook, } from "./types.js"; -import { initHooks } from "./registration.js"; - export class SDKHooks implements Hooks { sdkInitHooks: SDKInitHook[] = []; beforeCreateRequestHooks: BeforeCreateRequestHook[] = []; @@ -47,7 +45,6 @@ export class SDKHooks implements Hooks { this.registerAfterErrorHook(hook); } } - initHooks(this); } registerSDKInitHook(hook: SDKInitHook) { diff --git a/sdks/typescript/src/generated/hooks/registration.ts b/sdks/typescript/src/generated/hooks/registration.ts deleted file mode 100644 index 3a8fdeec..00000000 --- a/sdks/typescript/src/generated/hooks/registration.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Hooks } from "./types.js"; - -/* - * This file is only ever generated once on the first generation and then is free to be modified. - * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them - * in this file or in separate files in the hooks folder. - */ - -// @ts-ignore remove this line when you add your first hook and hooks is used -export function initHooks(hooks: Hooks) { - // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook - // with an instance of a hook that implements that specific Hook interface - // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance -} diff --git a/ui/tests/control-store.spec.ts b/ui/tests/control-store.spec.ts index d18b6c28..6f56b695 100644 --- a/ui/tests/control-store.spec.ts +++ b/ui/tests/control-store.spec.ts @@ -68,10 +68,15 @@ test.describe('Control Store Modal', () => { await expect(modal.getByText('data-analysis-agent')).toBeVisible(); // One control has no usage and renders as an em dash await expect(modal.getByText('—')).toBeVisible(); - // Attribution is plain text, not links. - await expect( - modal.getByRole('link', { name: 'customer-support-bot' }) - ).toHaveCount(0); + // Agent attribution renders as navigation links. + const customerSupportLink = modal.getByRole('link', { + name: 'customer-support-bot', + }); + await expect(customerSupportLink).toHaveCount(1); + await expect(customerSupportLink).toHaveAttribute( + 'href', + '/agents/customer-support-bot' + ); }); test('can search for controls', async ({ mockedPage }) => {