openapi: 3.0.3
info:
  title: AxonFlow Policy Management API
  description: |
    REST API for managing dynamic policies in AxonFlow.

    Policies define conditions and actions for request processing, enabling:
    - Content filtering and DLP
    - User-based access controls
    - Risk scoring and blocking
    - Cost management

    ## Authentication
    The Agent (port 8080) is the single entry point for all client traffic
    (ADR-026). Clients authenticate at the Agent with HTTP Basic auth
    (`clientId:clientSecret`); the Agent proxies policy requests to the
    Orchestrator and stamps the `X-Tenant-ID` header from the authenticated
    client's tenant. When calling the Orchestrator directly (internal
    deployments only), supply `X-Tenant-ID` yourself. `X-User-ID` is optional
    and used for audit attribution.

    Note: `/api/v1/templates*` is NOT proxied by the Agent — the template
    endpoints are reachable only on the Orchestrator directly (or via the
    customer portal catch-all).

    ## Multi-tenancy
    Policies are isolated per tenant. Each API call operates only on policies
    belonging to the authenticated tenant.

    ## Related APIs
    - The static policy enforcement API (`/api/v1/static-policies*`) is served
      by the Agent and specified in `agent-api.yaml`.
    - Unified policy read/write (an aggregated static + dynamic view) lives on
      the Enterprise customer portal and is documented internally.
      **Enterprise only**.

    ## Terminology note
    A terminology migration renaming static→system and dynamic→tenant policy
    endpoints is planned (#1431). No alias routes exist yet; the paths below
    are current.
  version: 2.1.0
  contact:
    name: AxonFlow Support
    url: https://getaxonflow.com/support
  license:
    name: Business Source License 1.1
    url: https://github.com/getaxonflow/axonflow/blob/main/LICENSE
servers:
  - url: https://agent.getaxonflow.com
    description: Production (Agent single entry point, ADR-026)
  - url: http://localhost:8080
    description: Local development (Agent single entry point)
  - url: http://localhost:8081
    description: Orchestrator direct (internal only)
tags:
  - name: Dynamic Policies
    description: |
      Dynamic policy CRUD via the ADR-026 `/api/v1/dynamic-policies` surface
      (Orchestrator, proxied by the Agent). Accepts only policies whose
      category starts with `dynamic-` or `media-`.
  - name: Policies
    description: Dynamic policy CRUD operations (Orchestrator)
  - name: Testing
    description: Policy testing and validation
  - name: Simulation
    description: |
      Policy simulation, impact reports, and conflict detection.
      **Evaluation tier and above.**
  - name: Bulk Operations
    description: Import and export policies
  - name: Templates
    description: Policy templates for quick policy creation
paths:
  /api/v1/dynamic-policies:
    get:
      tags:
        - Dynamic Policies
      summary: List dynamic policies
      description: |
        Retrieve a paginated list of dynamic policies. Only policies whose
        category starts with `dynamic-` or `media-` are returned. When
        `type=media` is passed without a category, results are filtered to
        `media-*` categories.

        `limit` is the preferred pagination parameter; `page_size` is
        deprecated but still accepted.
      operationId: listDynamicPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: type
          in: query
          description: Filter by policy type
          schema:
            $ref: '#/components/schemas/PolicyType'
        - name: category
          in: query
          description: |
            Filter by category. Must start with `dynamic-` or `media-`
            (e.g., dynamic-risk, media-safety); other values are rejected
            with a validation error.
          schema:
            type: string
        - name: enabled
          in: query
          description: Filter by enabled status
          schema:
            type: boolean
        - name: search
          in: query
          description: Search in policy name and description
          schema:
            type: string
            maxLength: 100
        - name: page
          in: query
          description: Page number (1-indexed)
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: Items per page (preferred over the deprecated page_size)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: page_size
          in: query
          deprecated: true
          description: Items per page (deprecated — use limit instead)
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: sort_by
          in: query
          description: Sort field
          schema:
            type: string
            enum:
              - name
              - created_at
              - updated_at
              - priority
            default: created_at
        - name: sort_dir
          in: query
          description: Sort direction
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
      responses:
        '200':
          description: List of dynamic policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoliciesListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Dynamic Policies
      summary: Create a dynamic policy
      description: |
        Create a new dynamic policy. `category` is required and must start
        with `dynamic-` or `media-` (e.g., dynamic-risk, media-safety).
      operationId: createDynamicPolicy
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePolicyRequest'
      responses:
        '201':
          description: Dynamic policy created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            Tier validation failed (e.g., organization-tier policy without
            Enterprise license)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIError'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/import:
    post:
      tags:
        - Dynamic Policies
      summary: Import dynamic policies
      description: |
        Bulk import dynamic policies from JSON. Supports up to 100 policies
        per request. Every policy must have a category starting with
        `dynamic-` or `media-`.
      operationId: importDynamicPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportPoliciesRequest'
      responses:
        '200':
          description: Import results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportPoliciesResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/export:
    get:
      tags:
        - Dynamic Policies
      summary: Export dynamic policies
      description: |
        Export the tenant's dynamic policies as JSON. Only policies with a
        `dynamic-*` or `media-*` category are included.
      operationId: exportDynamicPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Exported dynamic policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportPoliciesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/effective:
    get:
      tags:
        - Dynamic Policies
      summary: Get effective dynamic policies
      description: |
        Returns the enabled dynamic policies for the tenant (both `dynamic-*`
        and `media-*` categories), sorted by priority ascending. Returns up to
        100 policies.
      operationId: getEffectiveDynamicPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Effective dynamic policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoliciesListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/{id}:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    get:
      tags:
        - Dynamic Policies
      summary: Get a dynamic policy
      description: |
        Retrieve a single dynamic policy by ID. Returns 404 if the policy
        exists but is not a dynamic policy (category not `dynamic-*`/`media-*`).
      operationId: getDynamicPolicy
      responses:
        '200':
          description: Dynamic policy details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags:
        - Dynamic Policies
      summary: Update a dynamic policy
      description: |
        Update an existing dynamic policy. Only provided fields are updated.
        If `category` is changed, the new value must still start with
        `dynamic-` or `media-`.
      operationId: updateDynamicPolicy
      parameters:
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePolicyRequest'
      responses:
        '200':
          description: Dynamic policy updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Tier validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIError'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags:
        - Dynamic Policies
      summary: Delete a dynamic policy
      description: >-
        Soft-delete a dynamic policy. The policy is marked as deleted but
        retained for audit purposes.
      operationId: deleteDynamicPolicy
      parameters:
        - $ref: '#/components/parameters/UserID'
      responses:
        '204':
          description: Dynamic policy deleted
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Tier validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIError'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/{id}/test:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    post:
      tags:
        - Testing
      summary: Test a dynamic policy
      description: |
        Evaluate a dynamic policy against sample input without executing
        actions. `query` is required.
      operationId: testDynamicPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestPolicyRequest'
      responses:
        '200':
          description: Test results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestPolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/dynamic-policies/{id}/versions:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    get:
      tags:
        - Dynamic Policies
      summary: Get dynamic policy version history
      description: >-
        Retrieve the complete version history of a dynamic policy for audit
        purposes
      operationId: getDynamicPolicyVersions
      responses:
        '200':
          description: Version history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyVersionResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies:
    get:
      tags:
        - Policies
      summary: List policies
      description: Retrieve a paginated list of policies with optional filtering
      operationId: listPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: type
          in: query
          description: Filter by policy type
          schema:
            $ref: '#/components/schemas/PolicyType'
        - name: enabled
          in: query
          description: Filter by enabled status
          schema:
            type: boolean
        - name: search
          in: query
          description: Search in policy name and description
          schema:
            type: string
            maxLength: 100
        - name: page
          in: query
          description: Page number (1-indexed)
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          description: Items per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: sort_by
          in: query
          description: Sort field
          schema:
            type: string
            enum:
              - name
              - created_at
              - updated_at
              - priority
            default: created_at
        - name: sort_dir
          in: query
          description: Sort direction
          schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
      responses:
        '200':
          description: List of policies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PoliciesListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Policies
      summary: Create a policy
      description: Create a new policy with conditions and actions
      operationId: createPolicy
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePolicyRequest'
            examples:
              contentFilter:
                summary: Content filtering policy
                value:
                  name: Block PII Access
                  description: >-
                    Prevent unauthorized access to personally identifiable
                    information
                  type: content
                  conditions:
                    - field: query
                      operator: contains_any
                      value:
                        - ssn
                        - social security
                        - credit card
                    - field: user.role
                      operator: not_in
                      value:
                        - admin
                        - compliance
                  actions:
                    - type: block
                      config:
                        message: Access to PII requires admin or compliance role
                  priority: 100
                  enabled: true
              riskScoring:
                summary: Risk-based blocking
                value:
                  name: High Risk Block
                  description: Block requests with risk score above threshold
                  type: risk
                  conditions:
                    - field: risk_score
                      operator: greater_than
                      value: 0.8
                  actions:
                    - type: block
                      config:
                        message: Request blocked due to high risk score
                    - type: alert
                      config:
                        channel: security-alerts
                  priority: 200
                  enabled: true
      responses:
        '201':
          description: Policy created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/{id}:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    get:
      tags:
        - Policies
      summary: Get a policy
      description: Retrieve a single policy by ID
      operationId: getPolicy
      responses:
        '200':
          description: Policy details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    put:
      tags:
        - Policies
      summary: Update a policy
      description: Update an existing policy. Only provided fields are updated.
      operationId: updatePolicy
      parameters:
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePolicyRequest'
      responses:
        '200':
          description: Policy updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags:
        - Policies
      summary: Delete a policy
      description: >-
        Soft-delete a policy. The policy is marked as deleted but retained for
        audit purposes.
      operationId: deletePolicy
      parameters:
        - $ref: '#/components/parameters/UserID'
      responses:
        '204':
          description: Policy deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/{id}/test:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    post:
      tags:
        - Testing
      summary: Test a policy
      description: |
        Evaluate a policy against sample input without executing actions.
        Useful for validating policy behavior before enabling.
      operationId: testPolicy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestPolicyRequest'
            example:
              query: Show me the customer's SSN and credit card
              user:
                email: analyst@company.com
                role: analyst
                department: sales
              request_type: query
              context:
                connector: salesforce
      responses:
        '200':
          description: Test results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestPolicyResponse'
              example:
                matched: true
                blocked: true
                actions:
                  - type: block
                    config:
                      message: Access to PII requires admin or compliance role
                    message: Access to PII requires admin or compliance role
                explanation: >-
                  Policy 'Block PII Access' matched: all 2 conditions evaluated
                  to true
                eval_time_ms: 0.45
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/{id}/versions:
    parameters:
      - $ref: '#/components/parameters/PolicyID'
      - $ref: '#/components/parameters/TenantID'
    get:
      tags:
        - Policies
      summary: Get policy version history
      description: Retrieve the complete version history of a policy for audit purposes
      operationId: getPolicyVersions
      responses:
        '200':
          description: Version history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyVersionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/import:
    post:
      tags:
        - Bulk Operations
      summary: Import policies
      description: |
        Bulk import policies from JSON. Supports up to 100 policies per request.

        Overwrite modes:
        - `skip`: Skip policies that already exist (by name)
        - `overwrite`: Update existing policies
        - `error`: Fail if any policy already exists
      operationId: importPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportPoliciesRequest'
      responses:
        '200':
          description: Import results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportPoliciesResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/export:
    get:
      tags:
        - Bulk Operations
      summary: Export policies
      description: Export all policies for the tenant as JSON
      operationId: exportPolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Exported policies
          headers:
            Content-Disposition:
              schema:
                type: string
              description: Attachment filename
              example: attachment; filename=policies-export.json
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportPoliciesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/simulate:
    post:
      tags:
        - Simulation
      summary: Simulate policies (dry run)
      description: |
        Run all active policies against the provided input as a dry run —
        no audit writes and no policy actions are applied.

        **Evaluation tier and above** — requires an Evaluation or Enterprise
        license. Daily simulation quotas apply per tier (unlimited on
        Enterprise); exceeding the quota returns 429.
      operationId: simulatePolicies
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SimulatePoliciesRequest'
      responses:
        '200':
          description: Simulation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulatePoliciesResponse'
        '400':
          description: Invalid request body or missing query
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '401':
          description: Missing tenant identification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '403':
          description: Requires an Evaluation or Enterprise license
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '429':
          description: Daily simulation limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
  /api/v1/policies/impact-report:
    post:
      tags:
        - Simulation
      summary: Generate a policy impact report
      description: |
        Test a single policy against multiple inputs and return aggregate
        match/block statistics plus per-input results.

        **Evaluation tier and above** — requires an Evaluation or Enterprise
        license. The number of inputs per request is capped per tier.
      operationId: generateImpactReport
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImpactReportRequest'
      responses:
        '200':
          description: Impact report
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImpactReportResponse'
        '400':
          description: >-
            Invalid request body, missing policy_id/inputs, or input limit
            exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '401':
          description: Missing tenant identification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '403':
          description: Requires an Evaluation or Enterprise license
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/conflicts:
    post:
      tags:
        - Simulation
      summary: Detect policy conflicts
      description: |
        Analyze the tenant's active policies for contradictions, shadows, and
        redundancies. Optionally scope the analysis to a single policy by
        passing `policy_id` in the request body (the body may be omitted
        entirely to check all policies).

        **Evaluation tier and above** — requires an Evaluation or Enterprise
        license. Counts against the daily simulation quota.
      operationId: detectPolicyConflicts
      parameters:
        - $ref: '#/components/parameters/TenantID'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyConflictRequest'
      responses:
        '200':
          description: Conflict detection results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyConflictResponse'
        '400':
          description: Missing X-Tenant-ID header or invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '403':
          description: Requires an Evaluation or Enterprise license
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '429':
          description: Daily simulation limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationError'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/policies/dynamic:
    get:
      tags:
        - Policies
      summary: List active dynamic policies (legacy)
      deprecated: true
      description: |
        **Legacy endpoint.** Returns the array of active dynamic policies
        visible to the CALLING tenant — its own plus the shared
        global/default baseline (no pagination). Policies owned by other
        tenants are never returned in the same response, and requests
        without a resolvable tenant are rejected with 401 (fail closed).

        The tenant scope is read from the `X-Tenant-ID` header. When the
        request arrives through the AxonFlow Agent, that header is set from
        the validated credential and overwrites any client-supplied value,
        so the caller cannot choose the scope. The orchestrator itself does
        not authenticate this route, so a caller with direct network access
        to the orchestrator can name a tenant — deploy the orchestrator on a
        private network behind the Agent.

        Prefer `GET /api/v1/dynamic-policies`.
      operationId: listActiveDynamicPoliciesLegacy
      responses:
        '200':
          description: Array of active dynamic policies visible to the calling tenant
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: true
                  description: Engine-internal dynamic policy representation
        '401':
          description: >-
            Tenant scope could not be resolved (missing gateway-stamped
            X-Tenant-ID); fails closed with no policy data
  /api/v1/policies/test:
    post:
      tags:
        - Testing
      summary: Test input against all active policies (legacy)
      deprecated: true
      description: |
        **Legacy endpoint.** Evaluates the supplied input against all active
        dynamic policies and returns the raw policy evaluation result. Prefer
        `POST /api/v1/policies/simulate` (dry-run semantics, tier-aware) or
        `POST /api/v1/dynamic-policies/{id}/test` (single-policy testing).
      operationId: testPoliciesLegacy
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: Sample query to test against
                user:
                  $ref: '#/components/schemas/SimulationUserContext'
                request_type:
                  type: string
                  description: Type of request being simulated
      responses:
        '200':
          description: Policy evaluation result
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  allowed:
                    type: boolean
                  applied_policies:
                    type: array
                    items:
                      type: string
                  risk_score:
                    type: number
                    format: float
                  severity:
                    type: string
                    description: >-
                      Highest severity of matched policies: critical, high,
                      medium, low
                  required_actions:
                    type: array
                    items:
                      type: string
                  processing_time_ms:
                    type: integer
                    format: int64
        '400':
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/APIError'
  /api/v1/templates:
    get:
      tags:
        - Templates
      summary: List policy templates
      description: |
        Retrieve available policy templates for quick policy creation.
        Templates provide pre-configured policies for common use cases like
        HIPAA compliance, GDPR data protection, and rate limiting.
      operationId: listTemplates
      parameters:
        - $ref: '#/components/parameters/TenantID'
        - name: category
          in: query
          description: Filter by template category
          schema:
            type: string
            enum:
              - general
              - security
              - compliance
              - content_safety
              - rate_limiting
              - access_control
              - data_protection
              - custom
        - name: search
          in: query
          description: Search in name and description
          schema:
            type: string
        - name: tags
          in: query
          description: Comma-separated tags to filter by
          schema:
            type: string
        - name: active
          in: query
          description: Filter by active status
          schema:
            type: boolean
        - name: builtin
          in: query
          description: Filter by builtin status
          schema:
            type: boolean
        - name: page
          in: query
          description: Page number (1-indexed)
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: page_size
          in: query
          description: Number of items per page
          schema:
            type: integer
            minimum: 1
            default: 20
            maximum: 100
      responses:
        '200':
          description: List of templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplatesListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/templates/{id}:
    parameters:
      - $ref: '#/components/parameters/TemplateID'
      - $ref: '#/components/parameters/TenantID'
    get:
      tags:
        - Templates
      summary: Get a template
      description: Retrieve a single policy template by ID
      operationId: getTemplate
      responses:
        '200':
          description: Template details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/templates/{id}/apply:
    parameters:
      - $ref: '#/components/parameters/TemplateID'
      - $ref: '#/components/parameters/TenantID'
    post:
      tags:
        - Templates
      summary: Apply a template
      description: |
        Create a new policy from a template by providing variable values.
        Templates may have required and optional variables that customize
        the generated policy.
      operationId: applyTemplate
      parameters:
        - $ref: '#/components/parameters/UserID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApplyTemplateRequest'
            example:
              policy_name: Production Rate Limit
              description: Rate limiting for production API
              variables:
                threshold: 1000
                window_seconds: 60
              enabled: true
              priority: 75
      responses:
        '201':
          description: Policy created from template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApplyTemplateResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/templates/categories:
    get:
      tags:
        - Templates
      summary: List template categories
      description: Retrieve all available template categories
      operationId: listTemplateCategories
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: List of categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: string
                    example:
                      - general
                      - security
                      - compliance
                      - content_safety
                      - rate_limiting
                      - access_control
                      - data_protection
                      - custom
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/v1/templates/stats:
    get:
      tags:
        - Templates
      summary: Get template usage statistics
      description: Retrieve usage statistics for templates in your tenant
      operationId: getTemplateStats
      parameters:
        - $ref: '#/components/parameters/TenantID'
      responses:
        '200':
          description: Template statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateStatsResponse'
              example:
                stats:
                  - template_id: hipaa_phi_protection
                    template_name: HIPAA PHI Protection
                    usage_count: 15
                    last_used_at: '2025-01-15T14:30:00Z'
                  - template_id: gdpr_data_protection
                    template_name: GDPR Data Protection
                    usage_count: 8
                    last_used_at: '2025-01-14T09:15:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  parameters:
    TenantID:
      name: X-Tenant-ID
      in: header
      required: true
      description: |
        Tenant identifier for multi-tenancy isolation. When calling through
        the Agent (recommended), this header is stamped automatically from the
        authenticated client — you do not set it yourself. Required only on
        direct Orchestrator calls (internal deployments).
      schema:
        type: string
        example: tenant_abc123
    UserID:
      name: X-User-ID
      in: header
      required: false
      description: User identifier for audit logging
      schema:
        type: string
        example: user@company.com
    PolicyID:
      name: id
      in: path
      required: true
      description: Policy unique identifier
      schema:
        type: string
        example: pol_abc123def456
    TemplateID:
      name: id
      in: path
      required: true
      description: Template unique identifier
      schema:
        type: string
        example: hipaa_phi_protection
  schemas:
    PolicyType:
      type: string
      enum:
        - content
        - user
        - risk
        - cost
        - context_aware
        - media
        - rate-limit
        - budget
        - time-access
        - role-access
        - mcp
        - connector
      description: >
        Policy type determines evaluation context:

        - `content`: Evaluates request/response content

        - `user`: Evaluates user attributes

        - `risk`: Evaluates risk scores

        - `cost`: Evaluates cost estimates

        - `context_aware`: Context-aware controls (tenant isolation, debug
        restriction, sensitive-data control)

        - `media`: Media governance policies (multimodal image governance)

        - `rate-limit`, `budget`, `time-access`: MCP rate/budget controls

        - `role-access`, `mcp`, `connector`: MCP access controls
    ConditionOperator:
      type: string
      enum:
        - equals
        - not_equals
        - contains
        - not_contains
        - contains_any
        - regex
        - greater_than
        - less_than
        - in
        - not_in
      description: Comparison operator for conditions
    ActionType:
      type: string
      enum:
        - block
        - require_approval
        - redact
        - warn
        - alert
        - log
        - route
        - modify_risk
      description: |
        Action to take when policy matches:
        - `block`: Block the request with message
        - `require_approval`: Hold the request for human approval (HITL)
        - `redact`: Redact sensitive content from response
        - `warn`: Allow the request but attach a warning
        - `alert`: Send alert to configured channel
        - `log`: Log to audit trail
        - `route`: Route to specific provider
        - `modify_risk`: Adjust risk score
    PolicyCondition:
      type: object
      required:
        - field
        - operator
        - value
      properties:
        field:
          type: string
          description: |
            Field to evaluate. `media.*` fields apply to media governance
            policies (multimodal image governance); `step.*` fields are
            retry-aware workflow step fields for WCP policies.
          enum:
            - query
            - response
            - user.email
            - user.role
            - user.department
            - user.tenant_id
            - risk_score
            - request_type
            - connector
            - cost_estimate
            - media.has_faces
            - media.face_count
            - media.has_biometric_data
            - media.nsfw_score
            - media.violence_score
            - media.content_safe
            - media.document_type
            - media.is_sensitive_document
            - media.has_pii
            - media.pii_types
            - media.has_extracted_text
            - media.extracted_text_length
            - step.gate_count
            - step.completion_count
            - step.prior_completion_status
            - step.prior_output_available
            - step.last_decision
            - step.first_attempt_age_seconds
            - step.idempotency_key
        operator:
          $ref: '#/components/schemas/ConditionOperator'
        value:
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: array
              items:
                type: string
          description: Value to compare against
    PolicyAction:
      type: object
      required:
        - type
      properties:
        type:
          $ref: '#/components/schemas/ActionType'
        config:
          type: object
          additionalProperties: true
          description: Action-specific configuration
          example:
            message: Request blocked by policy
            channel: security-alerts
    PolicyResource:
      type: object
      properties:
        id:
          type: string
          description: Unique policy identifier
          example: pol_abc123def456
        name:
          type: string
          description: Human-readable policy name
          minLength: 3
          maxLength: 100
          example: Block PII Access
        description:
          type: string
          description: Detailed policy description
          maxLength: 500
          example: Prevent unauthorized access to personally identifiable information
        type:
          $ref: '#/components/schemas/PolicyType'
        category:
          type: string
          description: >-
            Policy category (dynamic-risk, dynamic-compliance, media-safety,
            etc.)
          example: dynamic-risk
        tier:
          type: string
          enum:
            - system
            - organization
            - tenant
          description: Policy tier in the hierarchy (system policies are immutable)
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyCondition'
          minItems: 1
          description: Conditions that must all match (AND logic)
        actions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyAction'
          minItems: 1
          description: Actions to execute when policy matches
        priority:
          type: integer
          minimum: 0
          maximum: 1000
          default: 0
          description: Higher priority policies are evaluated first
        enabled:
          type: boolean
          default: true
          description: Whether the policy is active
        version:
          type: integer
          minimum: 1
          description: Policy version number, incremented on each update
        tenant_id:
          type: string
          description: Owning tenant ID
        organization_id:
          type: string
          description: Organization ID (for organization-tier policies)
        tags:
          type: array
          items:
            type: string
          description: Tags for categorization
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
        created_by:
          type: string
          description: User who created the policy
        updated_by:
          type: string
          description: User who last updated the policy
        deleted_at:
          type: string
          format: date-time
          description: Soft-delete timestamp (present only on deleted policies)
    CreatePolicyRequest:
      type: object
      required:
        - name
        - type
        - conditions
        - actions
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          maxLength: 500
        type:
          $ref: '#/components/schemas/PolicyType'
        category:
          type: string
          description: >
            Policy category (dynamic-risk, dynamic-compliance, media-safety,
            etc.).

            Required on the /api/v1/dynamic-policies surface, where it must

            start with `dynamic-` or `media-`.
        tier:
          type: string
          enum:
            - organization
            - tenant
          description: Policy tier. Only organization or tenant is allowed via the API.
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyCondition'
          minItems: 1
        actions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyAction'
          minItems: 1
        priority:
          type: integer
          minimum: 0
          maximum: 1000
          default: 0
        enabled:
          type: boolean
          default: true
        tags:
          type: array
          items:
            type: string
          description: Tags for categorization
    UpdatePolicyRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 100
        description:
          type: string
          maxLength: 500
        type:
          $ref: '#/components/schemas/PolicyType'
        category:
          type: string
          description: Policy category. Only changeable on non-system policies.
        conditions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyCondition'
        actions:
          type: array
          items:
            $ref: '#/components/schemas/PolicyAction'
        priority:
          type: integer
          minimum: 0
          maximum: 1000
        enabled:
          type: boolean
        tags:
          type: array
          items:
            type: string
          description: Tags for categorization
    TestPolicyRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          description: Sample query to test against
        user:
          type: object
          additionalProperties: true
          description: User context for testing
          example:
            email: user@company.com
            role: analyst
            department: sales
        request_type:
          type: string
          description: Type of request being simulated
        context:
          type: object
          additionalProperties: true
          description: Additional context for testing
    TestPolicyResponse:
      type: object
      properties:
        matched:
          type: boolean
          description: Whether the policy conditions matched
        blocked:
          type: boolean
          description: Whether a block action would trigger
        actions:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              config:
                type: object
              message:
                type: string
          description: Actions that would be triggered
        explanation:
          type: string
          description: Human-readable explanation of the result
        eval_time_ms:
          type: number
          format: float
          description: Evaluation time in milliseconds
    ImportPoliciesRequest:
      type: object
      required:
        - policies
      properties:
        policies:
          type: array
          items:
            $ref: '#/components/schemas/CreatePolicyRequest'
          minItems: 1
          maxItems: 100
        overwrite_mode:
          type: string
          enum:
            - skip
            - overwrite
            - error
          default: skip
          description: How to handle existing policies
    ImportPoliciesResponse:
      type: object
      properties:
        created:
          type: integer
          description: Number of policies created
        updated:
          type: integer
          description: Number of policies updated
        skipped:
          type: integer
          description: Number of policies skipped
        errors:
          type: array
          items:
            type: string
          description: Error messages for failed imports
    ExportPoliciesResponse:
      type: object
      properties:
        policies:
          type: array
          items:
            $ref: '#/components/schemas/PolicyResource'
        exported_at:
          type: string
          format: date-time
        tenant_id:
          type: string
    PolicyVersionResponse:
      type: object
      properties:
        versions:
          type: array
          items:
            type: object
            properties:
              version:
                type: integer
              snapshot:
                $ref: '#/components/schemas/PolicyResource'
              changed_by:
                type: string
              changed_at:
                type: string
                format: date-time
              change_type:
                type: string
                enum:
                  - create
                  - update
                  - enable
                  - disable
                  - delete
              change_summary:
                type: string
    PolicyResponse:
      type: object
      properties:
        policy:
          $ref: '#/components/schemas/PolicyResource'
    PoliciesListResponse:
      type: object
      properties:
        policies:
          type: array
          items:
            $ref: '#/components/schemas/PolicyResource'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
    APIError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Error code
            message:
              type: string
              description: Human-readable error message
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string
              description: Field-level validation errors
    TemplateVariable:
      type: object
      required:
        - name
        - type
      properties:
        name:
          type: string
          description: Variable name used in template
          example: threshold
        type:
          type: string
          enum:
            - string
            - number
            - boolean
            - array
          description: Variable data type
        required:
          type: boolean
          default: false
          description: Whether this variable must be provided when applying the template
        default:
          description: Default value if not provided
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: array
              items:
                type: string
        description:
          type: string
          description: Human-readable description
          example: Maximum requests allowed per window
        validation:
          type: string
          description: Regex pattern for validating the variable value
    TemplateResource:
      type: object
      properties:
        id:
          type: string
          description: Unique template identifier
          example: hipaa_phi_protection
        name:
          type: string
          description: Template name (machine-readable)
          example: hipaa_phi_protection
        display_name:
          type: string
          description: Human-readable display name
          example: HIPAA PHI Protection
        description:
          type: string
          description: Template description
          example: Protects PHI data in accordance with HIPAA requirements
        category:
          type: string
          description: Template category
          example: compliance
        subcategory:
          type: string
          description: Template subcategory
          example: healthcare
        template:
          type: object
          description: Policy template with variable placeholders
          properties:
            type:
              $ref: '#/components/schemas/PolicyType'
            conditions:
              type: array
              items:
                $ref: '#/components/schemas/PolicyCondition'
            actions:
              type: array
              items:
                $ref: '#/components/schemas/PolicyAction'
        variables:
          type: array
          items:
            $ref: '#/components/schemas/TemplateVariable'
          description: Variables that can be customized
        is_builtin:
          type: boolean
          description: Whether this is a built-in template
        is_active:
          type: boolean
          description: Whether this template is active
        version:
          type: string
          description: Template version
          example: '1.0'
        tags:
          type: array
          items:
            type: string
          description: Tags for categorization
          example:
            - hipaa
            - healthcare
            - phi
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TemplatesListResponse:
      type: object
      properties:
        templates:
          type: array
          items:
            $ref: '#/components/schemas/TemplateResource'
        pagination:
          $ref: '#/components/schemas/PaginationMeta'
    TemplateResponse:
      type: object
      properties:
        template:
          $ref: '#/components/schemas/TemplateResource'
    ApplyTemplateRequest:
      type: object
      required:
        - policy_name
        - variables
      properties:
        policy_name:
          type: string
          minLength: 3
          maxLength: 100
          description: Name for the new policy
        description:
          type: string
          maxLength: 500
          description: Optional policy description
        variables:
          type: object
          additionalProperties: true
          description: Variable values for the template
        enabled:
          type: boolean
          default: false
          description: Whether to enable the policy immediately
        priority:
          type: integer
          minimum: 0
          maximum: 1000
          description: Policy priority (overrides template default)
    ApplyTemplateResponse:
      type: object
      properties:
        success:
          type: boolean
        policy:
          $ref: '#/components/schemas/PolicyResource'
        usage_id:
          type: string
          description: Unique ID for this template usage (for analytics)
        message:
          type: string
          description: Success message
    TemplateStatsResponse:
      type: object
      properties:
        stats:
          type: array
          items:
            type: object
            properties:
              template_id:
                type: string
              template_name:
                type: string
              usage_count:
                type: integer
              last_used_at:
                type: string
                format: date-time
    SimulationUserContext:
      type: object
      description: User context for policy simulation and testing
      properties:
        id:
          type: integer
          description: Numeric user identifier
        email:
          type: string
          example: analyst@company.com
        role:
          type: string
          example: analyst
        region:
          type: string
          description: User's region for geo-based routing policies
        permissions:
          type: array
          items:
            type: string
        tenant_id:
          type: string
        org_id:
          type: string
          description: Organization for multi-tenant isolation
    SimulationClientContext:
      type: object
      description: Client context for policy simulation
      properties:
        id:
          type: string
        name:
          type: string
        org_id:
          type: string
          description: Organization ID for usage tracking
        tenant_id:
          type: string
    SimulatePoliciesRequest:
      type: object
      required:
        - query
      properties:
        query:
          type: string
          description: Input to evaluate against all active policies
        request_type:
          type: string
          description: Type of request being simulated (defaults to "simulation")
        user:
          $ref: '#/components/schemas/SimulationUserContext'
        client:
          $ref: '#/components/schemas/SimulationClientContext'
        context:
          type: object
          additionalProperties: true
          description: Additional context for evaluation
    SimulatePoliciesResponse:
      type: object
      properties:
        allowed:
          type: boolean
          description: Whether the input would be allowed
        applied_policies:
          type: array
          items:
            type: string
          description: Names of policies that matched
        risk_score:
          type: number
          format: float
        required_actions:
          type: array
          items:
            type: string
        processing_time_ms:
          type: integer
          format: int64
        total_policies:
          type: integer
          description: |
            Number of active policies visible to the calling tenant — its own
            plus the shared global/default baseline.

            CHANGED: this previously counted every active policy in the
            deployment, across all tenants, which disclosed the deployment-wide
            policy count to every caller. Integrations that treated this as a
            deployment-level total will now see a smaller, tenant-scoped number.
        dry_run:
          type: boolean
          description: Always true — no actions were applied
        simulated_at:
          type: string
          format: date-time
        tier:
          type: string
          description: License tier the simulation ran under
        daily_usage:
          $ref: '#/components/schemas/SimulationDailyUsage'
    SimulationDailyUsage:
      type: object
      description: Simulation quota usage (omitted on unlimited tiers)
      properties:
        used:
          type: integer
        limit:
          type: integer
          description: Daily limit (-1 = unlimited)
    ImpactReportRequest:
      type: object
      required:
        - policy_id
        - inputs
      properties:
        policy_id:
          type: string
          description: Policy to test against the inputs
        inputs:
          type: array
          items:
            $ref: '#/components/schemas/ImpactReportInput'
          minItems: 1
          description: Test inputs (per-tier maximum applies)
    ImpactReportInput:
      type: object
      required:
        - query
      properties:
        query:
          type: string
        request_type:
          type: string
        user:
          type: object
          additionalProperties: true
          description: User context for testing
        context:
          type: object
          additionalProperties: true
    ImpactReportResponse:
      type: object
      properties:
        policy_id:
          type: string
        policy_name:
          type: string
        total_inputs:
          type: integer
        matched:
          type: integer
          description: Number of inputs that matched the policy
        blocked:
          type: integer
          description: Number of inputs that would be blocked
        match_rate:
          type: number
          format: float
        block_rate:
          type: number
          format: float
        results:
          type: array
          items:
            $ref: '#/components/schemas/ImpactReportResult'
        processing_time_ms:
          type: integer
          format: int64
        generated_at:
          type: string
          format: date-time
        tier:
          type: string
    ImpactReportResult:
      type: object
      properties:
        input_index:
          type: integer
        matched:
          type: boolean
        blocked:
          type: boolean
        actions:
          type: array
          items:
            type: string
          description: Action types that would trigger for this input
    PolicyConflictRequest:
      type: object
      properties:
        policy_id:
          type: string
          description: 'Optional: check a specific policy against all others'
    PolicyConflictResponse:
      type: object
      properties:
        conflicts:
          type: array
          items:
            $ref: '#/components/schemas/PolicyConflict'
        total_policies:
          type: integer
          description: Number of active policies analyzed
        conflict_count:
          type: integer
        checked_at:
          type: string
          format: date-time
        tier:
          type: string
    PolicyConflict:
      type: object
      description: A detected conflict between two policies
      properties:
        policy_a:
          $ref: '#/components/schemas/PolicyConflictRef'
        policy_b:
          $ref: '#/components/schemas/PolicyConflictRef'
        conflict_type:
          type: string
          enum:
            - contradictory_action
            - shadow
            - redundant
        description:
          type: string
        severity:
          type: string
          enum:
            - high
            - medium
            - low
        overlapping_field:
          type: string
          description: Condition field both policies evaluate
    PolicyConflictRef:
      type: object
      description: Identifies a policy in a conflict pair
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
    SimulationError:
      type: object
      description: Flat error shape returned by the simulation endpoints
      properties:
        error:
          type: string
          description: Error code (duplicated in code)
        code:
          type: string
        message:
          type: string
    PaginationMeta:
      type: object
      description: Pagination metadata
      properties:
        page:
          type: integer
          description: Current page number
          example: 1
        page_size:
          type: integer
          description: Items per page
          example: 20
        total_items:
          type: integer
          description: Total number of items
          example: 45
        total_pages:
          type: integer
          description: Total number of pages
          example: 3
  responses:
    Unauthorized:
      description: Missing or invalid tenant ID
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
          example:
            error:
              code: UNAUTHORIZED
              message: Missing tenant ID
    NotFound:
      description: Policy not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
          example:
            error:
              code: NOT_FOUND
              message: Policy not found
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
          example:
            error:
              code: VALIDATION_ERROR
              message: Request validation failed
              details:
                - field: name
                  message: Name must be between 3 and 100 characters
                - field: conditions[0].operator
                  message: >-
                    Invalid operator: like. Must be one of: equals, contains,
                    regex
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/APIError'
          example:
            error:
              code: INTERNAL_ERROR
              message: An unexpected error occurred
