openapi: 3.1.0
info:
  title: AI Gateway Runtime API
  version: 0.10.0
  description: Go rewrite contract. OpenAI-compatible payloads remain intentionally open.
servers:
  - url: /
paths:
  /healthz:
    get:
      operationId: getHealth
      responses:
        "200":
          description: Process is alive
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /readyz:
    get:
      operationId: getReadiness
      responses:
        "200":
          description: Required dependencies are ready
        "503":
          description: A required dependency is unavailable
  /metrics:
    get:
      operationId: getMetrics
      responses:
        "200": { description: Prometheus text metrics for runtime, audit, bootstrap and shadow traffic }
  /api/v1/admin/login:
    post:
      operationId: loginAdmin
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LoginRequest" }
      responses:
        "200": { description: Opaque Redis-backed session token }
        "401": { description: Invalid credentials }
        "429": { description: Account locked }
  /api/v1/admin/whoami:
    get:
      operationId: getAdminIdentity
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Active administrator identity and role }
        "401": { description: Invalid or expired session }
  /api/v1/admin/password:
    post:
      operationId: changeAdminPassword
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Administrator password changed after current-password verification }
        "401": { description: Current password invalid }
  /api/v1/admin/system-info:
    get:
      operationId: getSystemInfo
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Version, runtime, uptime and enabled resource counts } }
  /api/v1/admin/monitoring/overview:
    get:
      operationId: getMonitoringOverview
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: PostgreSQL-backed 24-hour request, token, cost and latency summary } }
  /api/v1/admin/reload:
    post:
      operationId: reloadRuntimeSnapshots
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Provider, content-policy and pricing snapshots reloaded } }
  /api/v1/admin/fact-check/settings:
    get:
      operationId: getFactCheckSettings
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Fact-check Provider reference, model and timeout without duplicated credentials } }
    put:
      operationId: updateFactCheckSettings
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Fact-check settings updated } }
  /api/v1/admin/fact-check/policies:
    get:
      operationId: listFactCheckPolicies
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Scoped fact-check policies } }
    post:
      operationId: createFactCheckPolicy
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Validated fact-check policy created } }
  /api/v1/admin/fact-check/policies/{policy_id}:
    put:
      operationId: updateFactCheckPolicy
      security: [{ bearerAuth: [] }]
      parameters: [{ name: policy_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Fact-check policy revision updated } }
    delete:
      operationId: deleteFactCheckPolicy
      security: [{ bearerAuth: [] }]
      parameters: [{ name: policy_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Fact-check policy deleted } }
  /api/v1/admin/fact-check/events:
    get:
      operationId: listFactCheckEvents
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Bounded fact-check event summaries } }
  /api/v1/admin/fact-check/events/{event_id}:
    get:
      operationId: getFactCheckEvent
      security: [{ bearerAuth: [] }]
      parameters: [{ name: event_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Fact-check claims and evidence detail } }
  /api/v1/admin/login/totp:
    post:
      operationId: completeAdminTOTPLogin
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TOTPLoginRequest" }
      responses:
        "200": { description: TOTP challenge completed and session issued }
        "401": { description: Invalid, reused, or consumed factor }
  /api/v1/admin/totp/status:
    get:
      operationId: getAdminTOTPStatus
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: TOTP enrollment status }
  /api/v1/admin/totp/setup:
    post:
      operationId: setupAdminTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: New secret and otpauth provisioning URI }
  /api/v1/admin/totp/confirm:
    post:
      operationId: confirmAdminTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Enrollment confirmed; backup codes returned once }
  /api/v1/admin/totp/disable:
    post:
      operationId: disableAdminTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: TOTP disabled after password and factor verification }
  /api/v1/admin/totp/backup-codes/regenerate:
    post:
      operationId: regenerateAdminTOTPBackupCodes
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Previous backup codes revoked and replacements returned once }
  /api/v1/admin/logout:
    post:
      operationId: logoutAdmin
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Session revoked }
  /api/v1/admin/menus:
    get:
      operationId: getAdminMenus
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Art Design Pro route tree for the active role }
  /api/v1/admin/identities/admins:
    get:
      operationId: listAdministrators
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Administrator accounts with direct and effective permissions }
        "403": { description: identity:manage permission required }
    post:
      operationId: createAdministrator
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IdentityInput" }
      responses:
        "200": { description: Administrator created and outbox event committed }
        "409": { description: Login already exists }
  /api/v1/admin/identities/admins/{identity_id}:
    put:
      operationId: updateAdministrator
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdentityID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IdentityInput" }
      responses:
        "200": { description: Account and permissions updated immediately }
        "409": { description: Self-disable, self-role change, or duplicate login }
  /api/v1/admin/identities/portal-users:
    get:
      operationId: listPortalUsers
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Portal accounts with direct and effective permissions }
    post:
      operationId: createPortalUser
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IdentityInput" }
      responses:
        "200": { description: Local portal account created }
  /api/v1/admin/identities/portal-users/{identity_id}:
    put:
      operationId: updatePortalUser
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/IdentityID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IdentityInput" }
      responses:
        "200": { description: Portal account updated }
  /api/v1/admin/departments:
    get:
      operationId: listDepartments
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Flat department catalog with parent names and active user counts }
    post:
      operationId: createDepartment
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DepartmentInput" }
      responses:
        "200": { description: Department created with outbox event }
        "409": { description: Duplicate code or invalid hierarchy }
  /api/v1/admin/departments/{department_id}:
    put:
      operationId: updateDepartment
      security: [{ bearerAuth: [] }]
      parameters:
        - name: department_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DepartmentInput" }
      responses:
        "200": { description: Department updated with hierarchy validation }
        "409": { description: Cycle or active users/children prevent the update }
  /api/v1/admin/identity-providers:
    get:
      operationId: listIdentityProviders
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: OIDC providers without client secret material }
    post:
      operationId: createIdentityProvider
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OIDCProviderInput" }
      responses:
        "200": { description: Encrypted OIDC provider configuration created }
  /api/v1/admin/identity-providers/{provider_id}:
    put:
      operationId: updateIdentityProvider
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ProviderID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OIDCProviderInput" }
      responses:
        "200": { description: OIDC provider updated; omitted secret remains unchanged }
  /api/v1/admin/saml-providers:
    get:
      operationId: listSAMLProviders
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: SAML identity provider configurations }
    post:
      operationId: createSAMLProvider
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SAMLProviderInput" }
      responses:
        "200": { description: SAML provider created after metadata and signing certificate validation }
  /api/v1/admin/saml-providers/{provider_id}:
    put:
      operationId: updateSAMLProvider
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ProviderID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SAMLProviderInput" }
      responses:
        "200": { description: SAML provider updated and revision incremented }
  /api/v1/admin/providers:
    get:
      operationId: listProviders
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Providers with masked credentials }
    post:
      operationId: createProvider
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderInput" }
      responses:
        "200": { description: Provider created and outbox event committed }
        "403": { description: Superadmin role required }
  /api/v1/admin/api-keys:
    get:
      operationId: listAPIKeys
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: API keys without secret material }
        "403": { description: Superadmin role required }
    post:
      operationId: createAPIKey
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/APIKeyInput" }
      responses:
        "200": { description: API key secret returned exactly once }
  /api/v1/admin/api-keys/{api_key_id}:
    delete:
      operationId: revokeAPIKey
      security: [{ bearerAuth: [] }]
      parameters:
        - name: api_key_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: Key revoked and shared cache invalidated }
        "404": { description: Key not found or already revoked }
  /api/v1/admin/api-keys/{api_key_id}/limits:
    put:
      operationId: updateAPIKeyLimits
      security: [{ bearerAuth: [] }]
      parameters:
        - name: api_key_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/APIKeyLimitsInput" }
      responses:
        "200": { description: Request and token limits updated; authentication cache invalidated }
        "404": { description: Key not found or already revoked }
  /api/v1/admin/model-routes:
    get:
      operationId: listModelRoutes
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Model alias and weighted route rules }
    post:
      operationId: createModelRoute
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ModelRouteInput" }
      responses:
        "200": { description: Route committed, snapshot refreshed, and peers notified }
        "409": { description: Duplicate route }
  /api/v1/admin/model-routes/{model_route_id}:
    put:
      operationId: updateModelRoute
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ModelRouteID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ModelRouteInput" }
      responses:
        "200": { description: Route updated and active snapshots refreshed }
        "404": { description: Route not found }
    delete:
      operationId: deleteModelRoute
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ModelRouteID"
      responses:
        "200": { description: Route deleted and active snapshots refreshed }
        "404": { description: Route not found }
  /api/v1/admin/audit-events:
    get:
      operationId: listAuditEvents
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: before, in: query, schema: { type: string, format: date-time } }
        - { name: api_key_id, in: query, schema: { type: string, format: uuid } }
        - { name: provider, in: query, schema: { type: string } }
        - { name: model, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: integer, minimum: 100, maximum: 599 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200": { description: Cursor-paginated gateway audit events }
        "403": { description: audit:read permission required }
  /api/v1/admin/usage/daily:
    get:
      operationId: listDailyUsage
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: api_key_id, in: query, schema: { type: string, format: uuid } }
        - { name: provider, in: query, schema: { type: string } }
        - { name: model, in: query, schema: { type: string } }
      responses:
        "200": { description: Daily request, failure, and token aggregates }
        "403": { description: usage:read permission required }
  /api/v1/admin/traces:
    get:
      operationId: listLLMTraces
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: trace_type, in: query, schema: { type: string, enum: [application, digital_employee] } }
        - { name: target_code, in: query, schema: { type: string } }
        - { name: request_id, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [running, success, error] } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200": { description: Metadata-only LLM Trace summaries }
        "403": { description: trace:read permission required }
  /api/v1/admin/traces/{trace_id}:
    get:
      operationId: getLLMTrace
      security: [{ bearerAuth: [] }]
      parameters: [{ name: trace_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200":
          description: Trace detail with ordered model, retrieval, and tool spans
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LLMTrace" }
        "403": { description: trace:read permission required }
        "404": { description: Trace not found }
  /api/v1/admin/agent-sessions:
    get:
      operationId: listAgentSessions
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
        - { name: trace_type, in: query, schema: { type: string, enum: [application, digital_employee] } }
        - { name: target_code, in: query, schema: { type: string, maxLength: 128 } }
        - { name: session_id, in: query, schema: { type: string, maxLength: 512 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } }
      responses:
        "200":
          description: Metadata-only application and digital-employee session aggregates
          content:
            application/json:
              schema:
                type: object
                required: [code, data]
                properties:
                  code: { type: integer }
                  data:
                    type: object
                    required: [items]
                    properties:
                      items: { type: array, items: { $ref: "#/components/schemas/AgentSession" } }
        "403": { description: trace:read permission required }
  /api/v1/admin/agent-nodes:
    get:
      operationId: listAgentNodes
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: Registered agent nodes with derived heartbeat status
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/AgentNode" } }
        "403": { description: agent_node:read permission required }
    post:
      operationId: createAgentNode
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentNodeInput" }
      responses:
        "200": { description: Node metadata and one-time registration token }
        "403": { description: agent_node:manage permission required }
        "409": { description: Node code already exists }
  /api/v1/admin/agent-nodes/route-preview:
    post:
      operationId: previewAgentNodeRoute
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentNodeRoutePreviewInput" }
      responses:
        "200":
          description: Read-only online candidate list and stable-hash selection; no remote task is dispatched
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentNodeRoutePreview" }
        "400": { description: Invalid pool, request key or capability constraints }
        "403": { description: agent_node:read permission required }
  /api/v1/admin/agent-nodes/{node_id}:
    put:
      operationId: updateAgentNode
      security: [{ bearerAuth: [] }]
      parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentNodeInput" }
      responses:
        "200": { description: Updated node metadata }
        "403": { description: agent_node:manage permission required }
        "404": { description: Node not found }
    delete:
      operationId: deleteAgentNode
      security: [{ bearerAuth: [] }]
      parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200": { description: Node removed and its token invalidated }
        "403": { description: agent_node:manage permission required }
        "404": { description: Node not found }
  /api/v1/admin/agent-nodes/{node_id}/rotate-token:
    post:
      operationId: rotateAgentNodeToken
      security: [{ bearerAuth: [] }]
      parameters: [{ name: node_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200": { description: New one-time registration token; previous token is immediately invalid }
        "403": { description: agent_node:manage permission required }
        "404": { description: Node not found }
  /api/v1/agent/nodes/{code}/heartbeat:
    post:
      operationId: heartbeatAgentNode
      security: [{ agentTokenAuth: [] }]
      parameters: [{ name: code, in: path, required: true, schema: { type: string, pattern: "^[a-z0-9][a-z0-9._-]{0,127}$" } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentNodeHeartbeat" }
      responses:
        "200": { description: Heartbeat accepted and derived online status returned }
        "401": { description: Invalid or disabled node token }
  /api/v1/admin/outbox-events:
    get:
      operationId: listOutboxEvents
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, dead, processed] } }
        - { name: type, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
      responses:
        "200": { description: Pending, dead-lettered, or processed transactional outbox events }
        "403": { description: outbox:read permission required }
  /api/v1/admin/outbox-events/{event_id}/retry:
    post:
      operationId: retryOutboxEvent
      security: [{ bearerAuth: [] }]
      parameters:
        - name: event_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { description: Pending or dead-lettered event made immediately claimable }
        "403": { description: outbox:manage permission required }
        "404": { description: Unprocessed event not found }
  /api/v1/admin/content-policies:
    get:
      operationId: listContentPolicies
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Ordered content policy definitions }
        "403": { description: content_policy:read permission required }
    post:
      operationId: createContentPolicy
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContentPolicyInput" }
      responses:
        "200": { description: Policy committed, compiled, and activated locally }
        "400": { description: Invalid RE2 rule or policy scope }
  /api/v1/admin/content-policies/{policy_id}:
    put:
      operationId: updateContentPolicy
      security: [{ bearerAuth: [] }]
      parameters: [{ name: policy_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContentPolicyInput" }
      responses:
        "200": { description: Policy revision updated and snapshot reloaded }
        "404": { description: Policy not found }
    delete:
      operationId: deleteContentPolicy
      security: [{ bearerAuth: [] }]
      parameters: [{ name: policy_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200": { description: Policy deleted and snapshot reloaded }
  /api/v1/admin/model-prices:
    get:
      operationId: listModelPrices
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Versioned model price definitions }
        "403": { description: pricing:read permission required }
    post:
      operationId: createModelPrice
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ModelPriceInput" }
      responses:
        "200": { description: Price version created and runtime snapshot reloaded }
  /api/v1/admin/model-prices/{price_id}:
    put:
      operationId: updateModelPrice
      security: [{ bearerAuth: [] }]
      parameters: [{ name: price_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ModelPriceInput" }
      responses:
        "200": { description: Price revision updated and runtime snapshot reloaded }
    delete:
      operationId: deleteModelPrice
      security: [{ bearerAuth: [] }]
      parameters: [{ name: price_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses:
        "200": { description: Price version deleted }
  /api/v1/admin/prompts:
    get:
      operationId: listPrompts
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Prompt templates and active immutable versions } }
    post:
      operationId: createPrompt
      security: [{ bearerAuth: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PromptInput" } } } }
      responses: { "200": { description: Template and initial immutable version created } }
  /api/v1/admin/prompts/{prompt_id}:
    get:
      operationId: getPrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Prompt metadata and current immutable version } }
    put:
      operationId: updatePromptMetadata
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PromptInput" } } } }
      responses: { "200": { description: Prompt metadata updated without mutating version content } }
    delete:
      operationId: deletePrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Prompt and versions deleted } }
  /api/v1/admin/prompts/{prompt_id}/versions:
    get:
      operationId: listPromptVersions
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Immutable Prompt version history } }
    post:
      operationId: createPromptVersion
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: New immutable version created and optionally activated } }
  /api/v1/admin/prompts/{prompt_id}/versions/{version}/activate:
    post:
      operationId: activatePromptVersion
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: version, in: path, required: true, schema: { type: integer, minimum: 1 } }
      responses: { "200": { description: Historical immutable version made current } }
  /api/v1/admin/prompts/{prompt_id}/render:
    post:
      operationId: previewPrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Current Prompt rendered for control-plane preview } }
  /api/v1/admin/prompt-categories:
    get:
      operationId: listPromptCategories
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Prompt categories with template counts } }
    post:
      operationId: createPromptCategory
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Prompt category created } }
  /api/v1/admin/prompt-categories/{category_id}:
    delete:
      operationId: deletePromptCategory
      security: [{ bearerAuth: [] }]
      parameters: [{ name: category_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Category removed and templates made uncategorized } }
  /api/v1/admin/knowledge-bases:
    get:
      operationId: listKnowledgeBases
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: PostgreSQL-backed knowledge bases } }
    post:
      operationId: createKnowledgeBase
      security: [{ bearerAuth: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/KnowledgeBaseInput" } } } }
      responses: { "200": { description: Knowledge base created } }
  /api/v1/admin/knowledge-bases/{knowledge_base_id}:
    put:
      operationId: updateKnowledgeBase
      security: [{ bearerAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/KnowledgeBaseInput" } } } }
      responses: { "200": { description: Knowledge base updated } }
    delete:
      operationId: deleteKnowledgeBase
      security: [{ bearerAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Unreferenced knowledge base deleted } }
  /api/v1/admin/knowledge-bases/{knowledge_base_id}/documents:
    get:
      operationId: listKnowledgeDocuments
      security: [{ bearerAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Bounded extracted-text documents } }
    post:
      operationId: addKnowledgeDocument
      security: [{ bearerAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Document stored in PostgreSQL and synchronously chunked } }
  /api/v1/admin/knowledge-bases/{knowledge_base_id}/documents/{document_id}:
    delete:
      operationId: deleteKnowledgeDocument
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: document_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses: { "200": { description: Document and chunks deleted } }
  /api/v1/admin/knowledge-bases/{knowledge_base_id}/documents/{document_id}/reprocess:
    post:
      operationId: reprocessKnowledgeDocument
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: document_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses: { "200": { description: Stored bounded text re-chunked transactionally } }
  /api/v1/admin/knowledge-bases/{knowledge_base_id}/search:
    post:
      operationId: testKnowledgeSearch
      security: [{ bearerAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Retriever test results } }
  /api/v1/admin/tools:
    get:
      operationId: listTools
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: HTTP tool definitions without decrypted secret headers } }
    post:
      operationId: createTool
      security: [{ bearerAuth: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ToolInput" } } } }
      responses: { "200": { description: SSRF-validated tool with encrypted headers created } }
  /api/v1/admin/tools/{tool_id}:
    put:
      operationId: updateTool
      security: [{ bearerAuth: [] }]
      parameters: [{ name: tool_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ToolInput" } } } }
      responses: { "200": { description: Tool revision updated } }
    delete:
      operationId: deleteTool
      security: [{ bearerAuth: [] }]
      parameters: [{ name: tool_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Unreferenced tool deleted } }
  /api/v1/admin/tools/{tool_id}/test:
    post:
      operationId: testTool
      security: [{ bearerAuth: [] }]
      parameters: [{ name: tool_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Bounded SSRF-safe administrative tool test } }
  /api/v1/admin/applications:
    get:
      operationId: listApplications
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Application drafts and current published versions } }
    post:
      operationId: createApplication
      security: [{ bearerAuth: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ApplicationInput" } } } }
      responses: { "200": { description: Application draft created } }
  /api/v1/admin/applications/catalog:
    get:
      operationId: getApplicationCompositionCatalog
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Prompt, knowledge and tool assets available for composition } }
  /api/v1/admin/applications/asset-dependencies:
    get:
      operationId: getApplicationAssetDependencies
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: asset_type, in: query, required: true, schema: { type: string, enum: [prompt, knowledge, tool] } }
        - { name: asset_id, in: query, required: true, schema: { type: string, format: uuid } }
      responses: { "200": { description: Draft and published application references to an asset } }
  /api/v1/admin/applications/{application_id}/publish:
    post:
      operationId: publishApplication
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Validated immutable application composition published } }
  /api/v1/admin/applications/{application_id}:
    get:
      operationId: getApplication
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Application composition and immutable version history } }
    put:
      operationId: updateApplication
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ApplicationInput" } } } }
      responses: { "200": { description: Application draft updated } }
    delete:
      operationId: deleteApplication
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Application and its versions deleted } }
  /api/v1/admin/applications/{application_id}/rollback/{version}:
    post:
      operationId: rollbackApplication
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: application_id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: version, in: path, required: true, schema: { type: integer, minimum: 1 } }
      responses: { "200": { description: Historical composition copied into a new immutable published version } }
  /api/v1/admin/applications/{application_id}/versions:
    get:
      operationId: listApplicationVersions
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Immutable application publication history } }
  /api/v1/admin/applications/{application_id}/runs:
    get:
      operationId: listApplicationRuns
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Recent application orchestration runs } }
  /api/v1/admin/notification-channels:
    get:
      operationId: listNotificationChannels
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Webhook subscriptions without decrypted signing secrets } }
    post:
      operationId: createNotificationChannel
      security: [{ bearerAuth: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/NotificationChannelInput" } } } }
      responses: { "200": { description: Signed Webhook subscription created } }
  /api/v1/admin/notification-deliveries:
    get:
      operationId: listNotificationDeliveries
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Durable Webhook delivery history } }
  /api/v1/admin/notification-channels/{channel_id}:
    put:
      operationId: updateNotificationChannel
      security: [{ bearerAuth: [] }]
      parameters: [{ name: channel_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/NotificationChannelInput" } } } }
      responses: { "200": { description: Notification channel updated } }
    delete:
      operationId: deleteNotificationChannel
      security: [{ bearerAuth: [] }]
      parameters: [{ name: channel_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Notification channel and deliveries deleted } }
  /api/v1/admin/notification-deliveries/{delivery_id}/retry:
    post:
      operationId: retryNotificationDelivery
      security: [{ bearerAuth: [] }]
      parameters: [{ name: delivery_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Failed delivery attempted immediately } }
  /v1/prompts:
    get:
      operationId: listRuntimePrompts
      security: [{ apiKeyAuth: [] }]
      responses: { "200": { description: Visible enabled Prompt definitions without template bodies } }
  /v1/prompts/{prompt_name}/render:
    post:
      operationId: renderPrompt
      security: [{ apiKeyAuth: [] }]
      parameters: [{ name: prompt_name, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Active template rendered after required-variable validation } }
  /v1/knowledge/{knowledge_base_id}/search:
    post:
      operationId: searchKnowledge
      security: [{ apiKeyAuth: [] }]
      parameters: [{ name: knowledge_base_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Department-scoped Retriever results } }
  /v1/knowledge/search:
    post:
      operationId: searchKnowledgeLegacyShape
      security: [{ apiKeyAuth: [] }]
      responses: { "200": { description: Knowledge search with knowledge_base_id in the JSON body } }
  /v1/tools:
    get:
      operationId: listRuntimeTools
      security: [{ apiKeyAuth: [] }]
      responses: { "200": { description: Department-scoped tool schemas without encrypted headers } }
  /v1/tools/{tool_code}/invoke:
    post:
      operationId: invokeTool
      security: [{ apiKeyAuth: [] }]
      parameters: [{ name: tool_code, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Schema-validated, SSRF-safe bounded HTTP tool result } }
  /v1/applications/{application_code}/chat/completions:
    post:
      operationId: runApplication
      security: [{ apiKeyAuth: [] }]
      parameters: [{ name: application_code, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Prompt, retrieval and bounded tool loop executed through the governed gateway } }
  /api/v1/admin/providers/{provider_id}:
    put:
      operationId: updateProvider
      security: [{ bearerAuth: [] }]
      parameters:
        - name: provider_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderInput" }
      responses:
        "200": { description: Provider updated and revision incremented }
        "404": { description: Provider not found }
  /api/v1/admin/providers/{provider_id}/test:
    post:
      operationId: testProviderConnection
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ProviderID"
      responses:
        "200":
          description: Bounded connectivity test against the upstream models endpoint
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderConnectionResult" }
        "404": { description: Provider not found }
        "502": { description: Upstream network request failed }
  /api/v1/admin/providers/{provider_id}/models:
    get:
      operationId: listProviderModels
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ProviderID"
      responses:
        "200":
          description: Current and historical provider model catalog
        "404": { description: Provider not found }
  /api/v1/admin/providers/{provider_id}/models/sync:
    post:
      operationId: syncProviderModels
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/ProviderID"
      responses:
        "200":
          description: Model catalog transactionally synchronized and outbox event committed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ModelSyncResult" }
        "404": { description: Provider not found }
        "502": { description: Upstream response failed validation }
  /api/v1/admin/providers/credentials/rotate:
    post:
      operationId: rotateProviderCredentials
      description: Re-encrypt every Provider credential not already using the active KEK version.
      security: [{ bearerAuth: [] }]
      responses:
        "200":
          description: All required credential updates and outbox events committed atomically
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CredentialRotationResult" }
  /api/v1/portal/login:
    post:
      operationId: loginPortal
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LoginRequest" }
      responses:
        "200": { description: Opaque Redis-backed portal session token }
        "401": { description: Invalid credentials }
  /api/v1/portal/sso/providers:
    get:
      operationId: listPublicSSOProviders
      responses:
        "200": { description: Enabled OIDC and SAML providers safe for login-page display }
  /api/v1/portal/sso/{provider_code}/start:
    get:
      operationId: startSSOLogin
      parameters:
        - name: provider_code
          in: path
          required: true
          schema: { type: string }
      responses:
        "302": { description: Redirect to the OIDC authorization endpoint or SAML Redirect-binding SSO endpoint }
  /api/v1/portal/sso/{provider_code}/callback:
    get:
      operationId: completeOIDCLogin
      parameters:
        - name: provider_code
          in: path
          required: true
          schema: { type: string }
      responses:
        "302": { description: Verified identity redirects with a one-minute single-use exchange code }
        "401": { description: State or ID Token validation failed }
    post:
      operationId: completeSAMLLogin
      parameters:
        - name: provider_code
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [SAMLResponse, RelayState]
              properties:
                SAMLResponse: { type: string, format: byte }
                RelayState: { type: string }
      responses:
        "302": { description: Signed assertion accepted and browser redirected with a single-use exchange code }
        "401": { description: Signature, request correlation, assertion, or replay validation failed }
  /api/v1/portal/sso/{provider_code}/metadata:
    get:
      operationId: getSAMLServiceProviderMetadata
      parameters:
        - name: provider_code
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: SAML Service Provider metadata XML }
        "404": { description: SAML provider not found }
  /api/v1/portal/sso/exchange:
    post:
      operationId: exchangeOIDCCode
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200": { description: Redis-backed portal session token }
        "401": { description: Exchange code invalid, expired, or already consumed }
  /api/v1/portal/me:
    get:
      operationId: getPortalIdentity
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Active portal identity }
  /api/v1/portal/login/totp:
    post:
      operationId: completePortalTOTPLogin
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TOTPLoginRequest" }
      responses:
        "200": { description: TOTP challenge completed and session issued }
        "401": { description: Invalid, reused, or consumed factor }
  /api/v1/portal/totp/status:
    get:
      operationId: getPortalTOTPStatus
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: TOTP enrollment status }
  /api/v1/portal/totp/setup:
    post:
      operationId: setupPortalTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: New secret and otpauth provisioning URI }
  /api/v1/portal/totp/confirm:
    post:
      operationId: confirmPortalTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Enrollment confirmed; backup codes returned once }
  /api/v1/portal/totp/disable:
    post:
      operationId: disablePortalTOTP
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: TOTP disabled after password and factor verification }
  /api/v1/portal/totp/backup-codes/regenerate:
    post:
      operationId: regeneratePortalTOTPBackupCodes
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Previous backup codes revoked and replacements returned once }
  /api/v1/portal/logout:
    post:
      operationId: logoutPortal
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Session revoked }
  /api/v1/portal/menus:
    get:
      operationId: getPortalMenus
      security: [{ bearerAuth: [] }]
      responses:
        "200": { description: Art Design Pro portal route tree }
  /api/v1/portal/password:
    post:
      operationId: changePortalPassword
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Local password changed after current-password verification } }
  /api/v1/portal/applications:
    get:
      operationId: listPortalApplications
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Published applications visible to the portal user's department } }
  /api/v1/portal/catalog:
    get:
      operationId: getPortalCatalog
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Department-scoped application and AI asset catalog } }
  /api/v1/portal/knowledge:
    get:
      operationId: listPortalKnowledge
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Enabled visible knowledge bases } }
  /api/v1/portal/tools:
    get:
      operationId: listPortalTools
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Enabled visible tool metadata without endpoint or secret headers } }
  /api/v1/portal/prompts:
    get:
      operationId: listPortalPrompts
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Enabled visible prompts with favorite state } }
  /api/v1/portal/prompts/{prompt_id}:
    get:
      operationId: getPortalPrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Visible prompt and current immutable content } }
  /api/v1/portal/prompts/{prompt_id}/favorite:
    post:
      operationId: favoritePortalPrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Prompt added to favorites idempotently } }
    delete:
      operationId: unfavoritePortalPrompt
      security: [{ bearerAuth: [] }]
      parameters: [{ name: prompt_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Prompt removed from favorites idempotently } }
  /api/v1/portal/model-requests/available:
    get:
      operationId: listPortalAvailableModels
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Enabled provider model catalog } }
  /api/v1/portal/model-requests:
    get:
      operationId: listPortalModelRequests
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Current user's model access requests } }
    post:
      operationId: createPortalModelRequest
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Pending model access request created with outbox event } }
  /api/v1/portal/logs:
    get:
      operationId: listPortalAuditEvents
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Bounded audit events owned through portal API keys } }
  /api/v1/portal/logs/{audit_event_id}:
    get:
      operationId: getPortalAuditEvent
      security: [{ bearerAuth: [] }]
      parameters: [{ name: audit_event_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Owned audit event with bounded previews } }
  /api/v1/portal/stats:
    get:
      operationId: getPortalStats
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Personal requests, tokens, cost and latency aggregate } }
  /api/v1/portal/cost:
    get:
      operationId: getPortalCost
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Personal month-to-date cost in integer microunits } }
  /api/v1/portal/docs-info:
    get:
      operationId: getPortalIntegrationDocs
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Safe runtime protocol and authentication overview } }
  /api/v1/portal/apps/{application_code}/chat:
    post:
      operationId: runPortalApplicationCompatibilityChat
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_code, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Single user message executed with a server-held encrypted application credential } }
  /api/v1/portal/apps/{application_code}/conversations:
    post:
      operationId: createPortalApplicationConversation
      security: [{ bearerAuth: [] }]
      parameters: [{ name: application_code, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: Server-managed conversation created } }
  /api/v1/portal/apps/{application_code}/conversations/{conversation_id}:
    get:
      operationId: getPortalApplicationConversation
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: application_code, in: path, required: true, schema: { type: string } }
        - { name: conversation_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses: { "200": { description: Ownership-checked conversation with verified immutable hash chain } }
  /api/v1/portal/apps/{application_code}/conversations/{conversation_id}/messages:
    post:
      operationId: appendPortalApplicationConversationMessage
      security: [{ bearerAuth: [] }]
      parameters:
        - { name: application_code, in: path, required: true, schema: { type: string } }
        - { name: conversation_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses: { "200": { description: Leased serial message execution with verified server-side history } }
  /api/v1/admin/models:
    get:
      operationId: listAdministrativeModels
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Enabled provider model catalog } }
  /api/v1/admin/model-requests:
    get:
      operationId: listAdministrativeModelRequests
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Portal model access requests by status } }
  /api/v1/admin/scheduled-tasks:
    get:
      operationId: listScheduledTasks
      security: [{ bearerAuth: [] }]
      responses: { "200": { description: Scheduled task definitions with next and last execution state } }
    post:
      operationId: createScheduledTask
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScheduledTaskInput" }
      responses: { "200": { description: Task created with KEK-encrypted execution credential } }
  /api/v1/admin/scheduled-tasks/{task_id}:
    get:
      operationId: getScheduledTask
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Task detail without plaintext credentials } }
    put:
      operationId: updateScheduledTask
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScheduledTaskInput" }
      responses: { "200": { description: Task updated and next execution recomputed } }
    delete:
      operationId: deleteScheduledTask
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Task and execution history deleted } }
  /api/v1/admin/scheduled-tasks/{task_id}/start:
    post:
      operationId: startScheduledTask
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Task enabled and next execution computed } }
  /api/v1/admin/scheduled-tasks/{task_id}/pause:
    post:
      operationId: pauseScheduledTask
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Task paused without deleting history } }
  /api/v1/admin/scheduled-tasks/{task_id}/run:
    post:
      operationId: runScheduledTaskNow
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Manual execution durably queued } }
  /api/v1/admin/scheduled-tasks/{task_id}/runs:
    get:
      operationId: listScheduledTaskRuns
      security: [{ bearerAuth: [] }]
      parameters: [{ name: task_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Execution history with status, response and error } }
  /api/v1/admin/model-requests/{request_id}/approve:
    post:
      operationId: approveModelRequest
      security: [{ bearerAuth: [] }]
      parameters: [{ name: request_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Pending request approved transactionally } }
  /api/v1/admin/model-requests/{request_id}/reject:
    post:
      operationId: rejectModelRequest
      security: [{ bearerAuth: [] }]
      parameters: [{ name: request_id, in: path, required: true, schema: { type: string, format: uuid } }]
      responses: { "200": { description: Pending request rejected transactionally } }
  /v1/models:
    get:
      operationId: listModels
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GatewayProvider"
      responses:
        "200": { description: OpenAI-compatible model list }
        "429": { description: Per-minute, monthly request, or monthly token quota exceeded }
        "503": { description: Authentication, admission control, or provider configuration unavailable }
  /v1/chat/completions:
    post:
      operationId: createChatCompletion
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GatewayProvider"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200": { description: JSON response or SSE stream }
        "429": { description: Per-minute, monthly request, or monthly token quota exceeded }
        "503": { description: Authentication, admission control, or provider configuration unavailable }
  /v1/responses:
    post:
      operationId: createResponse
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GatewayProvider"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200": { description: JSON response or SSE stream }
        "429": { description: Per-minute, monthly request, or monthly token quota exceeded }
        "503": { description: Authentication, admission control, or provider configuration unavailable }
  /v1/embeddings:
    post:
      operationId: createEmbedding
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GatewayProvider"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200": { description: OpenAI-compatible embedding response }
        "429": { description: Per-minute, monthly request, or monthly token quota exceeded }
        "503": { description: Authentication, admission control, or provider configuration unavailable }
  /v1/messages:
    post:
      operationId: createAnthropicMessage
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: "#/components/parameters/GatewayProvider"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200": { description: Anthropic-compatible response or SSE stream }
        "429": { description: Per-minute, monthly request, or monthly token quota exceeded }
        "503": { description: Authentication, admission control, or provider configuration unavailable }
components:
  parameters:
    ModelRouteID:
      name: model_route_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Enables bounded retries for replayable POST requests. Reusing a key is the caller's idempotency contract.
      schema: { type: string, minLength: 1, maxLength: 255 }
    IdentityID:
      name: identity_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ProviderID:
      name: provider_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    GatewayProvider:
      name: X-Gateway-Provider
      in: header
      required: false
      description: Enabled database Provider code. Omit to use the current default snapshot entry.
      schema: { type: string }
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
    apiKeyAuth:
      type: http
      scheme: bearer
      description: Gateway API Key with gateway:invoke scope; X-Gateway-API-Key is also accepted.
    agentTokenAuth:
      type: apiKey
      in: header
      name: X-Agent-Token
      description: One-time-issued node registration token used for heartbeats.
  schemas:
    Health:
      type: object
      required: [status, version]
      properties:
        status: { type: string, const: ok }
        version: { type: string }
    LoginRequest:
      type: object
      required: [userName, password]
      properties:
        userName: { type: string, minLength: 1, maxLength: 128 }
        password: { type: string, minLength: 1, maxLength: 1024 }
    TOTPLoginRequest:
      type: object
      required: [temp_token]
      properties:
        temp_token: { type: string, minLength: 1 }
        code: { type: string, pattern: "^[0-9]{6}$" }
        backup_code: { type: string, pattern: "^[A-Za-z2-9]{4}-?[A-Za-z2-9]{4}$" }
      anyOf:
        - required: [code]
        - required: [backup_code]
    ProviderInput:
      type: object
      required: [code, adapter, base_url, capabilities, config, enabled]
      properties:
        code:
          type: string
          pattern: "^[a-z][a-z0-9_-]{1,63}$"
        adapter:
          type: string
          enum: [openai-compatible]
        base_url: { type: string, format: uri }
        api_key:
          type: string
          description: Omit during update to retain the existing encrypted credential.
        capabilities:
          type: array
          items: { type: string }
        config: { type: object, additionalProperties: true }
        enabled: { type: boolean }
    IdentityInput:
      type: object
      required: [login, display_name, role, permissions, active]
      properties:
        login: { type: string, minLength: 2, maxLength: 128 }
        display_name: { type: string, maxLength: 64 }
        role: { type: string, enum: [superadmin, operator, auditor, member] }
        password:
          type: string
          minLength: 12
          maxLength: 1024
          description: Required on create; omit on update to retain the current password.
        permissions:
          type: array
          uniqueItems: true
          items:
            type: string
            pattern: "^[a-z][a-z0-9_.:-]{2,127}$|^\\*$"
        active: { type: boolean }
        department_id:
          type: [string, "null"]
          format: uuid
          description: Portal accounts may bind to an active department.
    DepartmentInput:
      type: object
      required: [code, name, description, active]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 1024 }
        parent_id: { type: [string, "null"], format: uuid }
        active: { type: boolean }
    OIDCProviderInput:
      type: object
      required:
        [code, display_name, issuer_url, client_id, redirect_uri, portal_return_url, scopes, auto_provision, enabled]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        display_name: { type: string, minLength: 1, maxLength: 128 }
        issuer_url: { type: string, format: uri }
        client_id: { type: string, minLength: 1 }
        client_secret:
          type: string
          description: Required on create and omitted on update to retain the encrypted value.
        redirect_uri: { type: string, format: uri }
        portal_return_url: { type: string, format: uri }
        scopes:
          type: array
          contains: { const: openid }
          items: { type: string }
        auto_provision: { type: boolean }
        default_department_id: { type: [string, "null"], format: uuid }
        enabled: { type: boolean }
    SAMLProviderInput:
      type: object
      required:
        [code, display_name, metadata_url, sp_entity_id, acs_url, portal_return_url, email_attribute, name_attribute, auto_provision, enabled]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        display_name: { type: string, minLength: 1, maxLength: 128 }
        metadata_url: { type: string, format: uri }
        sp_entity_id:
          type: string
          minLength: 1
          maxLength: 512
          description: Absolute URI used as the SAML audience.
        acs_url:
          type: string
          format: uri
          description: Must end with /api/v1/portal/sso/{code}/callback.
        portal_return_url: { type: string, format: uri }
        email_attribute: { type: string, maxLength: 256 }
        name_attribute: { type: string, maxLength: 256 }
        auto_provision: { type: boolean }
        default_department_id: { type: [string, "null"], format: uuid }
        enabled: { type: boolean }
    ProviderConnectionResult:
      type: object
      required: [connected, latency_ms, message]
      properties:
        connected: { type: boolean }
        status_code: { type: integer }
        latency_ms: { type: integer, format: int64, minimum: 0 }
        message: { type: string }
    ProviderModel:
      type: object
      required:
        [id, provider_id, provider_model_id, metadata, enabled, discovered_at, last_seen_at]
      properties:
        id: { type: string, format: uuid }
        provider_id: { type: string, format: uuid }
        provider_model_id: { type: string, minLength: 1, maxLength: 512 }
        owned_by: { type: string }
        metadata: { type: object, additionalProperties: true }
        enabled: { type: boolean }
        discovered_at: { type: string, format: date-time }
        last_seen_at: { type: string, format: date-time }
    ModelSyncResult:
      type: object
      required: [discovered, active, disabled, synced_at]
      properties:
        discovered: { type: integer, minimum: 0 }
        active: { type: integer, minimum: 0 }
        disabled: { type: integer, minimum: 0 }
        synced_at: { type: string, format: date-time }
    CredentialRotationResult:
      type: object
      required: [active_version, loaded_versions, rotated, skipped]
      properties:
        active_version: { type: integer, minimum: 1 }
        loaded_versions:
          type: array
          items: { type: integer, minimum: 1 }
        rotated: { type: integer, minimum: 0 }
        skipped: { type: integer, minimum: 0 }
    APIKeyInput:
      type: object
      required: [name, scopes, requests_per_minute, monthly_request_quota, monthly_token_quota]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        scopes:
          type: array
          minItems: 1
          items: { type: string, enum: [gateway:invoke, "*"] }
        requests_per_minute:
          type: integer
          minimum: 0
          maximum: 1000000
          description: Per-API-key UTC minute request limit; zero means unlimited.
        monthly_request_quota:
          type: integer
          format: int64
          minimum: 0
          maximum: 1000000000000
          description: Accepted requests per UTC calendar month; zero means unlimited.
        monthly_token_quota:
          type: integer
          format: int64
          minimum: 0
          maximum: 1000000000000000
          description: Input plus output tokens per UTC calendar month; zero means unlimited.
        expires_at: { type: [string, "null"], format: date-time }
    APIKeyLimitsInput:
      type: object
      required: [requests_per_minute, monthly_request_quota, monthly_token_quota]
      properties:
        requests_per_minute: { type: integer, minimum: 0, maximum: 1000000 }
        monthly_request_quota: { type: integer, format: int64, minimum: 0, maximum: 1000000000000 }
        monthly_token_quota: { type: integer, format: int64, minimum: 0, maximum: 1000000000000000 }
    ModelRouteInput:
      type: object
      required: [name, source_model, target_model, provider_id, weight, priority, conditions, enabled]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        source_model: { type: string, minLength: 1, maxLength: 512 }
        target_model: { type: string, minLength: 1, maxLength: 512 }
        provider_id: { type: string, format: uuid }
        weight: { type: integer, minimum: 1, maximum: 10000 }
        priority: { type: integer, minimum: -100000, maximum: 100000 }
        enabled: { type: boolean }
        conditions:
          type: object
          additionalProperties: true
          properties:
            endpoints:
              type: array
              items: { type: string, enum: [/v1/chat/completions, /v1/responses, /v1/embeddings, /v1/messages] }
            api_key_ids:
              type: array
              items: { type: string, format: uuid }
            tenant_ids:
              type: array
              items: { type: string, format: uuid }
    ContentPolicyInput:
      type: object
      required: [name, description, action, priority, paths, models, api_key_ids, rules, enabled]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 1000 }
        action: { type: string, enum: [audit, block, redact] }
        priority: { type: integer, minimum: -100000, maximum: 100000 }
        paths: { type: array, items: { type: string } }
        models: { type: array, items: { type: string } }
        api_key_ids: { type: array, items: { type: string, format: uuid } }
        enabled: { type: boolean }
        rules:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: object
            required: [name, pattern, replacement]
            properties:
              name: { type: string, minLength: 1 }
              pattern: { type: string, minLength: 1, maxLength: 512, description: Go RE2 expression }
              replacement: { type: string }
    ModelPriceInput:
      type: object
      required: [provider_code, model_pattern, input_microunits_per_million, output_microunits_per_million, currency, effective_from, enabled]
      properties:
        provider_code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        model_pattern: { type: string, description: Exact model name or a prefix ending in one asterisk }
        input_microunits_per_million: { type: integer, format: int64, minimum: 0 }
        output_microunits_per_million: { type: integer, format: int64, minimum: 0 }
        currency: { type: string, minLength: 3, maxLength: 3, default: USD }
        effective_from: { type: string, format: date-time }
        effective_to: { type: [string, "null"], format: date-time }
        enabled: { type: boolean }
    PromptVariable:
      type: object
      required: [name, required]
      properties:
        name: { type: string, pattern: "^[A-Za-z_][A-Za-z0-9_]*$" }
        label: { type: string, maxLength: 128 }
        default: { type: string, maxLength: 10000 }
        required: { type: boolean }
    PromptInput:
      type: object
      required: [name, description, tags, department_ids, enabled]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        category_id: { type: [string, "null"], format: uuid }
        tags: { type: array, maxItems: 30, items: { type: string } }
        department_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        enabled: { type: boolean }
        content: { type: string, maxLength: 100000, description: Required only when creating the initial version. }
        variables: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/PromptVariable" } }
        change_note: { type: string }
    KnowledgeBaseInput:
      type: object
      required: [name, description, retrieval_mode, chunk_size, chunk_overlap, department_ids, enabled]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        retrieval_mode: { type: string, enum: [postgres_fts] }
        chunk_size: { type: integer, minimum: 200, maximum: 8000 }
        chunk_overlap: { type: integer, minimum: 0 }
        department_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        enabled: { type: boolean }
    ToolInput:
      type: object
      required: [code, name, description, endpoint_url, http_method, input_schema, timeout_seconds, department_ids, enabled]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        endpoint_url: { type: string, format: uri }
        http_method: { type: string, enum: [GET, POST, PUT, PATCH, DELETE] }
        headers:
          type: object
          writeOnly: true
          additionalProperties: { type: string }
          description: Omit during update to preserve the existing KEK-encrypted headers.
        input_schema: { type: object, additionalProperties: true }
        timeout_seconds: { type: integer, minimum: 1, maximum: 120 }
        department_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        enabled: { type: boolean }
    ApplicationConfig:
      type: object
      required: [model, knowledge_base_ids, tool_ids, retrieval_top_k, temperature, max_tool_rounds]
      properties:
        model: { type: string, maxLength: 255 }
        prompt_template_id: { type: string, format: uuid }
        knowledge_base_ids: { type: array, maxItems: 20, items: { type: string, format: uuid } }
        tool_ids: { type: array, maxItems: 20, items: { type: string, format: uuid } }
        retrieval_top_k: { type: integer, minimum: 1, maximum: 20 }
        temperature: { type: number, minimum: 0, maximum: 2 }
        max_tool_rounds: { type: integer, minimum: 0, maximum: 8 }
    ApplicationInput:
      type: object
      required: [code, name, description, status, department_ids, draft_config]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{2,63}$" }
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        status: { type: string, enum: [draft, active, suspended, retired] }
        department_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        draft_config: { $ref: "#/components/schemas/ApplicationConfig" }
    NotificationChannelInput:
      type: object
      required: [name, webhook_url, event_patterns, enabled]
      properties:
        name: { type: string, minLength: 1, maxLength: 128 }
        webhook_url: { type: string, format: uri }
        signing_secret:
          type: string
          writeOnly: true
          maxLength: 4096
          description: Omit during update to preserve the existing KEK-encrypted signing secret.
        event_patterns:
          type: array
          minItems: 1
          maxItems: 100
          items: { type: string, description: Exact event type or a prefix ending in one asterisk. }
        enabled: { type: boolean }
    ScheduledTaskInput:
      type: object
      required: [code, name, description, cron_expression, timezone, target_type, target_code, prompt, variables, skill_ids, mcp_server_ids, conversation_id, enabled]
      properties:
        code: { type: string, pattern: "^[a-z][a-z0-9_-]{1,63}$" }
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        cron_expression: { type: string, maxLength: 128, description: Standard five-field cron expression. }
        timezone: { type: string, maxLength: 128, example: Asia/Shanghai }
        target_type: { type: string, enum: [application, digital_employee] }
        target_code: { type: string, maxLength: 64 }
        prompt: { type: string, minLength: 1, maxLength: 100000 }
        variables: { type: object, additionalProperties: true }
        skill_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        mcp_server_ids: { type: array, maxItems: 100, items: { type: string, format: uuid } }
        conversation_id: { type: string, maxLength: 128 }
        notification_channel_id: { type: [string, "null"], format: uuid }
        api_key:
          type: string
          writeOnly: true
          maxLength: 512
          description: Required on create; omit on update to retain the encrypted credential.
        enabled: { type: boolean }
    LLMTraceSpan:
      type: object
      required: [id, trace_id, span_type, name, status, started_at, input_tokens, output_tokens, round, error, metadata]
      properties:
        id: { type: string, format: uuid }
        trace_id: { type: string, format: uuid }
        parent_id: { type: string, format: uuid }
        span_type: { type: string, enum: [model, tool, retrieval] }
        name: { type: string }
        status: { type: string, enum: [running, success, error] }
        started_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time }
        latency_ms: { type: integer, minimum: 0 }
        provider_code: { type: string }
        model: { type: string }
        input_tokens: { type: integer, minimum: 0 }
        output_tokens: { type: integer, minimum: 0 }
        round: { type: integer, minimum: 0 }
        error: { type: string }
        metadata: { type: object, additionalProperties: true }
    LLMTrace:
      type: object
      required: [id, request_id, trace_type, target_code, conversation_id, status, started_at, retrieval_count, model_call_count, tool_call_count, error, metadata]
      properties:
        id: { type: string, format: uuid }
        request_id: { type: string }
        api_key_id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        trace_type: { type: string, enum: [application, digital_employee] }
        target_id: { type: string, format: uuid }
        target_code: { type: string }
        conversation_id: { type: string }
        status: { type: string, enum: [running, success, error] }
        started_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time }
        latency_ms: { type: integer, minimum: 0 }
        retrieval_count: { type: integer, minimum: 0 }
        model_call_count: { type: integer, minimum: 0 }
        tool_call_count: { type: integer, minimum: 0 }
        error: { type: string }
        metadata: { type: object, additionalProperties: true }
        spans: { type: array, items: { $ref: "#/components/schemas/LLMTraceSpan" } }
    AgentSession:
      type: object
      required: [id, trace_type, target_code, trace_count, latest_trace_id, latest_status, started_at, updated_at, retrieval_count, model_call_count, tool_call_count]
      properties:
        id: { type: string, description: Session key; request-derived for stateless calls }
        trace_type: { type: string, enum: [application, digital_employee] }
        target_code: { type: string }
        trace_count: { type: integer, minimum: 1 }
        latest_trace_id: { type: string, format: uuid }
        latest_status: { type: string, enum: [running, success, error] }
        started_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        retrieval_count: { type: integer, minimum: 0 }
        model_call_count: { type: integer, minimum: 0 }
        tool_call_count: { type: integer, minimum: 0 }
    AgentNodeInput:
      type: object
      required: [name, description, endpoint, node_type, pool_type, pool_code, enabled]
      properties:
        code: { type: string, pattern: "^[a-z0-9][a-z0-9._-]{0,127}$" }
        name: { type: string, minLength: 1, maxLength: 128 }
        description: { type: string, maxLength: 4000 }
        endpoint: { type: string, maxLength: 512 }
        node_type: { type: string, enum: [worker, gateway, executor] }
        pool_type: { type: string, enum: [public, private] }
        pool_code: { type: string, minLength: 1, maxLength: 64 }
        enabled: { type: boolean }
    AgentNodeRoutePreviewInput:
      type: object
      required: [pool_type, pool_code, request_key]
      properties:
        pool_type: { type: string, enum: [public, private] }
        pool_code: { type: string, minLength: 1, maxLength: 64 }
        required_capabilities: { type: array, maxItems: 32, items: { type: string, maxLength: 128 } }
        request_key: { type: string, minLength: 1, maxLength: 512 }
    AgentNodeHeartbeat:
      type: object
      required: [version, capabilities, metadata, error]
      properties:
        version: { type: string, maxLength: 128 }
        capabilities: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        error: { type: string, maxLength: 4000 }
    AgentNode:
      type: object
      required: [id, code, name, description, endpoint, node_type, pool_type, pool_code, enabled, status, token_prefix, version, capabilities, metadata, last_error, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        description: { type: string }
        endpoint: { type: string }
        node_type: { type: string, enum: [worker, gateway, executor] }
        pool_type: { type: string, enum: [public, private] }
        pool_code: { type: string }
        enabled: { type: boolean }
        status: { type: string, enum: [pending, online, offline, disabled] }
        token_prefix: { type: string }
        version: { type: string }
        capabilities: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        last_heartbeat_at: { type: string, format: date-time }
        last_heartbeat_ip: { type: string }
        last_error: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AgentNodeTokenResponse:
      type: object
      required: [node, token, warning]
      properties:
        node: { $ref: "#/components/schemas/AgentNode" }
        token: { type: string, writeOnly: true }
        warning: { type: string }
    AgentNodeRoutePreview:
      type: object
      required: [pool_type, pool_code, required_capabilities, request_key, selection_policy, reason, selected, candidates]
      properties:
        pool_type: { type: string, enum: [public, private] }
        pool_code: { type: string }
        required_capabilities: { type: array, items: { type: string } }
        request_key: { type: string }
        selection_policy: { type: string, example: stable-hash(request_key,node_id) }
        reason: { type: string, enum: [selected_online_node, no_online_node, no_capable_node] }
        selected:
          anyOf:
            - { $ref: "#/components/schemas/AgentNode" }
            - { type: 'null' }
        candidates: { type: array, items: { $ref: "#/components/schemas/AgentNode" } }
