# Akmatori REST API - published contract.
#
# Canonical URL: https://akmatori.com/api/openapi.yaml
# Human-readable reference: https://akmatori.com/docs/api
# Source of truth: docs/openapi.yaml in github.com/akmatori/akmatori
#
# Regenerate with `npm run openapi:sync` in the akmatori-frontend repository.

openapi: 3.1.0
info:
  title: Akmatori API
  description: |
    AI-powered AIOps platform API. Manages skills, tools, incidents, alert sources,
    and system settings for automated incident investigation and remediation.
  version: 1.0.0
  contact:
    name: Akmatori
    url: https://akmatori.com/contact
    email: admin@akmatori.com
  license:
    name: Apache-2.0
    identifier: Apache-2.0
servers:
  - url: /api
    description: >-
      Base path of API major version 1 on your Akmatori instance. The major
      version of this document is the major part of info.version.
  - url: https://your-akmatori-instance/api
    description: Self-hosted deployment (replace the host with your own).

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT token obtained from POST /auth/login

  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
        details:
          type: object
          additionalProperties:
            type: string
          description: Field-level validation errors

    Memory:
      type: object
      description: Cross-incident memory entry (host fact, recurring pattern, tool quirk, or operator feedback)
      properties:
        id: {type: integer}
        scope:
          type: string
          description: '"global" or a skill name'
        type:
          type: string
          enum: [host, incident_pattern, tool_quirk, feedback]
        name:
          type: string
          description: Slug-safe (a-z, 0-9, hyphen). Unique per (scope, name).
        description:
          type: string
          description: Short summary used in MEMORY.md manifest (≤500 chars)
        body:
          type: string
          description: Full memory body (≤8KB)
        incident_uuid:
          type: string
          description: UUID of the incident that produced this memory, if any
        created_by:
          type: string
          enum: [agent, operator]
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    MemoryRequest:
      type: object
      required: [scope, type, name, description]
      properties:
        scope: {type: string}
        type:
          type: string
          enum: [host, incident_pattern, tool_quirk, feedback]
        name: {type: string}
        description: {type: string}
        body: {type: string}
        incident_uuid: {type: string}
        created_by: {type: string, enum: [agent, operator]}

    Integration:
      type: object
      description: |
        A configured connection to a messaging provider (Slack, Telegram, ...).
        Credentials are stored as a JSON blob whose shape is provider-specific.
      properties:
        id: {type: integer}
        uuid: {type: string, format: uuid}
        provider:
          type: string
          enum: [slack, telegram]
        name: {type: string}
        credentials:
          type: object
          additionalProperties: true
        enabled: {type: boolean}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    IntegrationCreateRequest:
      type: object
      required: [provider, name]
      properties:
        provider:
          type: string
          enum: [slack, telegram]
        name: {type: string}
        credentials:
          type: object
          additionalProperties: true
        enabled: {type: boolean}

    IntegrationUpdateRequest:
      type: object
      description: Provider is immutable on update.
      properties:
        name: {type: string}
        credentials:
          type: object
          additionalProperties: true
        enabled: {type: boolean}

    Channel:
      type: object
      description: |
        A specific addressable destination within an Integration (a Slack
        channel, a Telegram chat). Capability flags determine which triggers
        can reference it.
      properties:
        id: {type: integer}
        uuid: {type: string, format: uuid}
        integration_id: {type: integer}
        external_id:
          type: string
          description: Provider-specific channel handle (e.g. Slack channel ID or name)
        display_name: {type: string}
        can_post: {type: boolean}
        can_listen: {type: boolean}
        is_default_post:
          type: boolean
          description: At most one is_default_post=true per provider is enforced.
        extraction_prompt:
          type: string
          description: Optional per-channel system prompt for inbound extraction
        process_human_messages: {type: boolean}
        enabled: {type: boolean}
        integration: {$ref: '#/components/schemas/Integration'}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ChannelCreateRequest:
      type: object
      required: [integration_uuid, external_id]
      properties:
        integration_uuid: {type: string, format: uuid}
        external_id: {type: string}
        display_name: {type: string}
        can_post: {type: boolean}
        can_listen: {type: boolean}
        is_default_post: {type: boolean}
        extraction_prompt: {type: string}
        process_human_messages: {type: boolean}
        enabled: {type: boolean}

    ChannelUpdateRequest:
      type: object
      description: Partial patch; integration cannot be changed after creation.
      properties:
        external_id: {type: string}
        display_name: {type: string}
        can_post: {type: boolean}
        can_listen: {type: boolean}
        is_default_post: {type: boolean}
        extraction_prompt: {type: string}
        process_human_messages: {type: boolean}
        enabled: {type: boolean}

    CronJob:
      type: object
      description: |
        A scheduled task. Each job runs on its own cron expression and always
        executes as a full agent investigation under the `cron-agent` system
        skill, posting results to a Channel. System cron jobs (`is_system=true`)
        are seeded at boot and cannot be deleted.
      properties:
        id: {type: integer}
        uuid: {type: string, format: uuid}
        name: {type: string}
        schedule:
          type: string
          description: Standard 5-field cron expression (m h dom mon dow)
        prompt: {type: string}
        is_system:
          type: boolean
          description: When true, this row was seeded by the platform and cannot be deleted (returns 409). All other fields remain editable.
        channel_id: {type: integer, nullable: true}
        enabled: {type: boolean}
        last_run_at: {type: string, format: date-time, nullable: true}
        last_run_status: {type: string, enum: [ok, error, ""]}
        last_run_error: {type: string}
        next_run_at: {type: string, format: date-time, nullable: true}
        channel:
          allOf: [{$ref: '#/components/schemas/Channel'}]
          nullable: true
          description: Resolved channel for outbound posts. Omitted when the cron has no `channel_id` assigned and no workspace-default channel can be resolved at read time.
        tools:
          type: array
          description: Per-cron tool allowlist. Always present (possibly empty). The cron-agent invocation is restricted to these ToolInstances; the global skill/tool settings are NOT inherited. Settings JSONB is intentionally omitted from this slim projection to keep secrets off the wire.
          items:
            $ref: '#/components/schemas/CronJobToolSummary'
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    CronJobToolSummary:
      type: object
      description: Slim, secret-free projection of a ToolInstance exposed on the cron-jobs endpoint.
      properties:
        id: {type: integer}
        name: {type: string}
        logical_name: {type: string}
        tool_type: {type: string}
        enabled: {type: boolean}

    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
          format: int64
        total_pages:
          type: integer

    Skill:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
          description: Kebab-case skill name
        description:
          type: string
        category:
          type: string
        is_system:
          type: boolean
        enabled:
          type: boolean
        prompt:
          type: string
          description: SKILL.md prompt content
        tools:
          type: array
          items:
            $ref: '#/components/schemas/ToolInstance'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ToolType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        schema:
          type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ToolInstance:
      type: object
      properties:
        id:
          type: integer
        tool_type_id:
          type: integer
        name:
          type: string
        logical_name:
          type: string
          description: Human-readable logical name for tool routing (auto-derived from name if empty)
        settings:
          type: object
        enabled:
          type: boolean
        tool_type:
          $ref: '#/components/schemas/ToolType'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Incident:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
          format: uuid
        source:
          type: string
        source_id:
          type: string
        source_kind:
          type: string
          enum: [alert, cron, slack_mention]
          description: Trigger kind that spawned this incident.
        source_uuid:
          type: string
          description: UUID of the triggering entity (AlertSourceInstance, CronJob, or originating Slack mention).
        title:
          type: string
        status:
          type: string
          enum: [pending, running, diagnosed, completed, failed]
        context:
          type: object
        session_id:
          type: string
        working_dir:
          type: string
        full_log:
          type: string
        response:
          type: string
        tokens_used:
          type: integer
        execution_time_ms:
          type: integer
          format: int64
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ContextFile:
      type: object
      properties:
        id:
          type: integer
        filename:
          type: string
        original_name:
          type: string
        mime_type:
          type: string
        size:
          type: integer
          format: int64
        description:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AlertSourceType:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        display_name:
          type: string
        description:
          type: string
        default_field_mappings:
          type: object
        webhook_secret_header:
          type: string
        deprecated:
          type: boolean
          description: When true, this source type is hidden from new-source pickers. Retained for legacy rows (e.g. the retired slack_channel type).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AlertSourceInstance:
      type: object
      properties:
        id:
          type: integer
        uuid:
          type: string
          format: uuid
        alert_source_type_id:
          type: integer
        name:
          type: string
        description:
          type: string
        webhook_secret:
          type: string
        field_mappings:
          type: object
        settings:
          type: object
        notification_channel_id:
          type: integer
          nullable: true
          description: Optional FK to a Channel that overrides the per-provider default for outbound posts.
        enabled:
          type: boolean
        alert_source_type:
          $ref: '#/components/schemas/AlertSourceType'
        notification_channel:
          $ref: '#/components/schemas/Channel'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    SSHKey:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        is_default:
          type: boolean
        created_at:
          type: string
          format: date-time

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
            example:
              error: Validation failed
              code: validation_error
              details:
                name: is required

paths:
  # ===== Authentication =====
  /auth/login:
    post:
      summary: Login
      operationId: login
      security: []
      tags: [Authentication]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  username:
                    type: string
                  expires_in:
                    type: integer
                    description: Token expiry in seconds
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /auth/verify:
    get:
      summary: Verify token
      operationId: verifyToken
      tags: [Authentication]
      responses:
        '200':
          description: Token is valid
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  username:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===== Skills =====
  /skills:
    get:
      summary: List skills
      operationId: listSkills
      tags: [Skills]
      responses:
        '200':
          description: List of skills with prompts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Skill'
    post:
      summary: Create skill
      operationId: createSkill
      tags: [Skills]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 64
                description:
                  type: string
                  maxLength: 1024
                category:
                  type: string
                  maxLength: 64
                prompt:
                  type: string
      responses:
        '201':
          description: Skill created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Skill'
        '400':
          $ref: '#/components/responses/BadRequest'

  /skills/{name}:
    parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get skill
      operationId: getSkill
      tags: [Skills]
      responses:
        '200':
          description: Skill with prompt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Skill'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update skill
      operationId: updateSkill
      tags: [Skills]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                description:
                  type: string
                category:
                  type: string
                enabled:
                  type: boolean
                prompt:
                  type: string
      responses:
        '200':
          description: Updated skill
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Skill'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete skill
      operationId: deleteSkill
      tags: [Skills]
      responses:
        '204':
          description: Skill deleted
        '403':
          description: Cannot delete system skill
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /skills/{name}/prompt:
    parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get skill prompt
      operationId: getSkillPrompt
      tags: [Skills]
      responses:
        '200':
          description: Prompt content
          content:
            application/json:
              schema:
                type: object
                properties:
                  prompt:
                    type: string
    put:
      summary: Update skill prompt
      operationId: updateSkillPrompt
      tags: [Skills]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
      responses:
        '200':
          description: Prompt updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string

  /skills/{name}/tools:
    parameters:
      - name: name
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get skill tools
      operationId: getSkillTools
      tags: [Skills]
      responses:
        '200':
          description: Tool instances assigned to skill
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ToolInstance'
    put:
      summary: Update skill tools
      operationId: updateSkillTools
      tags: [Skills]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tool_instance_ids:
                  type: array
                  items:
                    type: integer
      responses:
        '200':
          description: Updated skill
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Skill'

  /skills/sync:
    post:
      summary: Sync skills from filesystem
      operationId: syncSkills
      tags: [Skills]
      responses:
        '200':
          description: Sync completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  message:
                    type: string

  # ===== Tools =====
  /tool-types:
    get:
      summary: List tool types
      operationId: listToolTypes
      tags: [Tools]
      responses:
        '200':
          description: Available tool types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ToolType'

  /tools:
    get:
      summary: List tool instances
      operationId: listTools
      tags: [Tools]
      responses:
        '200':
          description: Tool instances
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ToolInstance'
    post:
      summary: Create tool instance
      operationId: createTool
      tags: [Tools]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tool_type_id, name]
              properties:
                tool_type_id:
                  type: integer
                name:
                  type: string
                logical_name:
                  type: string
                  description: Optional logical name; auto-derived from name if empty
                settings:
                  type: object
      responses:
        '201':
          description: Tool created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolInstance'

  /tools/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      summary: Get tool instance
      operationId: getTool
      tags: [Tools]
      responses:
        '200':
          description: Tool instance
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolInstance'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update tool instance
      operationId: updateTool
      tags: [Tools]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                logical_name:
                  type: string
                  description: Optional logical name; re-derived from name if empty
                settings:
                  type: object
                enabled:
                  type: boolean
      responses:
        '200':
          description: Updated tool
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ToolInstance'
    delete:
      summary: Delete tool instance
      operationId: deleteTool
      tags: [Tools]
      responses:
        '204':
          description: Tool deleted

  /tools/{id}/ssh-keys:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      summary: List SSH keys
      operationId: listSSHKeys
      tags: [Tools]
      responses:
        '200':
          description: SSH keys for tool
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SSHKey'
    post:
      summary: Add SSH key
      operationId: createSSHKey
      tags: [Tools]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, private_key]
              properties:
                name:
                  type: string
                private_key:
                  type: string
                is_default:
                  type: boolean
      responses:
        '201':
          description: Key added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SSHKey'
        '409':
          description: Key name already exists

  # ===== Incidents =====
  /incidents:
    get:
      summary: List incidents
      operationId: listIncidents
      tags: [Incidents]
      parameters:
        - name: from
          in: query
          schema:
            type: integer
            format: int64
          description: Start time (unix seconds)
        - name: to
          in: query
          schema:
            type: integer
            format: int64
          description: End time (unix seconds)
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
          description: Page number (enables pagination)
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
          description: Items per page (default 50, max 200)
      responses:
        '200':
          description: |
            Without pagination params: returns array of incidents (max 500).
            With page/per_page: returns paginated envelope.
          content:
            application/json:
              schema:
                oneOf:
                  - type: array
                    items:
                      $ref: '#/components/schemas/Incident'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Incident'
                      pagination:
                        $ref: '#/components/schemas/PaginationMeta'
    post:
      summary: Create incident
      operationId: createIncident
      tags: [Incidents]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [task]
              properties:
                task:
                  type: string
                context:
                  type: object
      responses:
        '201':
          description: Incident created
          content:
            application/json:
              schema:
                type: object
                properties:
                  uuid:
                    type: string
                  status:
                    type: string
                  working_dir:
                    type: string
                  message:
                    type: string

  /incidents/{uuid}:
    parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get incident
      operationId: getIncident
      tags: [Incidents]
      responses:
        '200':
          description: Incident details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Incident'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===== Settings =====
  /settings/slack:
    get:
      summary: Get Slack settings (removed)
      description: |
        Removed in the unified-channels rollout. Always returns 410 Gone.
        Use `/api/integrations` and `/api/channels` to manage Slack
        credentials and posting/listening destinations.
      deprecated: true
      operationId: getSlackSettings
      tags: [Settings]
      responses:
        '410':
          description: Endpoint removed; configure Slack via /api/integrations and /api/channels.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
    put:
      summary: Update Slack settings (removed)
      description: |
        Removed in the unified-channels rollout. Always returns 410 Gone.
        Use the Integrations + Channels CRUD APIs.
      deprecated: true
      operationId: updateSlackSettings
      tags: [Settings]
      responses:
        '410':
          description: Endpoint removed; configure Slack via /api/integrations and /api/channels.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}

  /settings/llm:
    get:
      summary: Get LLM settings
      operationId: getLLMSettings
      tags: [Settings]
      responses:
        '200':
          description: Per-provider LLM settings
    put:
      summary: Update LLM settings
      operationId: updateLLMSettings
      tags: [Settings]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [provider]
              properties:
                provider:
                  type: string
                  enum: [openai, anthropic, google, openrouter, custom]
                api_key:
                  type: string
                model:
                  type: string
                thinking_level:
                  type: string
                  enum: [off, minimal, low, medium, high, xhigh]
                base_url:
                  type: string
                  format: uri
                temperature:
                  type: number
                  nullable: true
                  minimum: 0
                  maximum: 2
                  description: >-
                    Sampling override. Omit to leave unchanged, null to clear.
                    When unset, no temperature is sent and the provider default applies.
                top_p:
                  type: number
                  nullable: true
                  minimum: 0
                  maximum: 1
                  description: Nucleus sampling override; null clears it back to the provider default.
                top_k:
                  type: integer
                  nullable: true
                  minimum: 1
                  maximum: 1000
                  description: >-
                    Top-k sampling override; null clears it. Dropped for providers whose
                    API has no top_k (OpenAI, Mistral).
                max_tokens:
                  type: integer
                  nullable: true
                  minimum: 1
                  maximum: 1000000
                  description: Output token cap override; null clears it back to the model default.
      responses:
        '200':
          description: Updated settings
        '400':
          description: Validation error (sampling parameter out of range, invalid provider/thinking_level/base_url)

  /settings/proxy:
    get:
      summary: Get proxy settings
      operationId: getProxySettings
      tags: [Settings]
      responses:
        '200':
          description: Proxy configuration
    put:
      summary: Update proxy settings
      operationId: updateProxySettings
      tags: [Settings]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                proxy_url:
                  type: string
                  format: uri
                no_proxy:
                  type: string
                services:
                  type: object
                  properties:
                    llm:
                      type: object
                      properties:
                        enabled:
                          type: boolean
                    slack:
                      type: object
                      properties:
                        enabled:
                          type: boolean
                    zabbix:
                      type: object
                      properties:
                        enabled:
                          type: boolean
      responses:
        '200':
          description: Updated proxy settings

  /settings/formatting:
    get:
      summary: Get response-formatting settings
      operationId: getFormattingSettings
      tags: [Settings]
      responses:
        '200':
          description: Singleton formatting settings record
    put:
      summary: Update response-formatting settings
      operationId: updateFormattingSettings
      tags: [Settings]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                  description: When true, the agent's final response is reformatted via a one-shot LLM call before being persisted to incident.response and posted to Slack.
                system_prompt:
                  type: string
                  maxLength: 8192
                  description: System prompt used by the formatter LLM call.
                max_tokens:
                  type: integer
                  minimum: 1
                  maximum: 8000
                temperature:
                  type: number
                  minimum: 0
                  maximum: 2
      responses:
        '200':
          description: Updated formatting settings
        '400':
          description: Validation error (max_tokens out of range, temperature out of range, or system_prompt over 8192 bytes)

  # ===== Context Files =====
  /context:
    get:
      summary: List context files
      operationId: listContextFiles
      tags: [Context]
      responses:
        '200':
          description: Context files
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ContextFile'
    post:
      summary: Upload context file
      operationId: uploadContextFile
      tags: [Context]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, filename]
              properties:
                file:
                  type: string
                  format: binary
                filename:
                  type: string
                description:
                  type: string
      responses:
        '201':
          description: File uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContextFile'

  /context/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      summary: Get context file metadata
      operationId: getContextFile
      tags: [Context]
      responses:
        '200':
          description: File metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContextFile'
    delete:
      summary: Delete context file
      operationId: deleteContextFile
      tags: [Context]
      responses:
        '204':
          description: File deleted

  /context/{id}/download:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
    get:
      summary: Download context file
      operationId: downloadContextFile
      tags: [Context]
      responses:
        '200':
          description: File content
          content:
            '*/*':
              schema:
                type: string
                format: binary

  /context/validate:
    post:
      summary: Validate file references
      operationId: validateReferences
      tags: [Context]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                  references:
                    type: array
                    items:
                      type: string
                  found:
                    type: array
                    items:
                      type: string
                  missing:
                    type: array
                    items:
                      type: string

  # ===== Alert Sources =====
  # ===== Messaging Integrations + Channels =====
  /integrations:
    get:
      summary: List messaging provider integrations
      operationId: listIntegrations
      tags: [Integrations]
      responses:
        '200':
          description: Configured integrations
          content:
            application/json:
              schema:
                type: array
                items: {$ref: '#/components/schemas/Integration'}
        '503':
          description: Channel service is not configured
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
    post:
      summary: Create a new integration
      operationId: createIntegration
      tags: [Integrations]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/IntegrationCreateRequest'}
      responses:
        '201':
          description: Integration created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Integration'}
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: Conflict (duplicate name or invariant violation)
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}

  /integrations/{uuid}:
    parameters:
      - name: uuid
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      summary: Get integration by UUID
      operationId: getIntegration
      tags: [Integrations]
      responses:
        '200':
          description: Integration details
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Integration'}
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update integration (provider is immutable)
      operationId: updateIntegration
      tags: [Integrations]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/IntegrationUpdateRequest'}
      responses:
        '200':
          description: Updated integration
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Integration'}
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete integration (cascades channels)
      operationId: deleteIntegration
      tags: [Integrations]
      responses:
        '204':
          description: Integration deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /channels:
    get:
      summary: List channels (optionally filtered)
      operationId: listChannels
      tags: [Channels]
      parameters:
        - name: integration_uuid
          in: query
          schema: {type: string, format: uuid}
          description: Filter to channels belonging to this integration
        - name: can_post
          in: query
          schema: {type: boolean}
          description: Filter to channels whose can_post flag matches
        - name: can_listen
          in: query
          schema: {type: boolean}
          description: Filter to channels whose can_listen flag matches
      responses:
        '200':
          description: Channels matching the filter
          content:
            application/json:
              schema:
                type: array
                items: {$ref: '#/components/schemas/Channel'}
        '503':
          description: Channel service is not configured
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
    post:
      summary: Create a new channel
      operationId: createChannel
      tags: [Channels]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ChannelCreateRequest'}
      responses:
        '201':
          description: Channel created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Channel'}
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: Integration not found
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '409':
          description: Default-post invariant violated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}

  /channels/{uuid}:
    parameters:
      - name: uuid
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      summary: Get channel by UUID
      operationId: getChannel
      tags: [Channels]
      responses:
        '200':
          description: Channel details
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Channel'}
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update channel
      operationId: updateChannel
      tags: [Channels]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ChannelUpdateRequest'}
      responses:
        '200':
          description: Updated channel
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Channel'}
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Default-post invariant violated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
    delete:
      summary: Delete channel
      operationId: deleteChannel
      tags: [Channels]
      responses:
        '204':
          description: Channel deleted
        '404':
          $ref: '#/components/responses/NotFound'

  /cron-jobs:
    get:
      summary: List scheduled cron jobs
      operationId: listCronJobs
      tags: [Cron Jobs]
      responses:
        '200':
          description: Configured cron jobs
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CronJob'
        '503':
          description: Cron service not configured
    post:
      summary: Create cron job
      operationId: createCronJob
      tags: [Cron Jobs]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name, schedule, prompt]
              properties:
                name: {type: string}
                schedule:
                  type: string
                  description: Standard 5-field cron expression (m h dom mon dow)
                prompt: {type: string}
                channel_uuid:
                  type: string
                  format: uuid
                  description: Optional channel to post to. Defaults to the workspace default channel.
                enabled: {type: boolean, default: true}
                tool_instance_ids:
                  type: array
                  description: Per-cron tool allowlist passed to the cron-agent invocation. Omitted or empty means the agent runs with no infrastructure tools (memory + runbooks only).
                  items: {type: integer}
              description: |
                Unknown fields (e.g. the legacy `mode` or `description`) are
                rejected with 400 — the handler decodes with
                `DisallowUnknownFields`.
      responses:
        '201':
          description: Cron job created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CronJob'}
        '400':
          description: Validation error (bad schedule, missing channel, unknown field, ...)
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '409':
          description: Duplicate cron job name.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '503':
          description: Cron service not configured

  /cron-jobs/{uuid}:
    parameters:
      - name: uuid
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      summary: Get cron job by UUID
      operationId: getCronJob
      tags: [Cron Jobs]
      responses:
        '200':
          description: Cron job details
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CronJob'}
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: Cron service not configured
    put:
      summary: Update cron job
      operationId: updateCronJob
      tags: [Cron Jobs]
      description: |
        Partial patch — only fields present in the body are applied. To leave
        the per-cron tool allowlist alone, omit `tool_instance_ids`; pass an
        explicit empty array to clear it. System cron jobs (`is_system=true`)
        accept all field updates including `enabled`, but cannot be deleted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: {type: string}
                schedule: {type: string}
                prompt: {type: string}
                channel_uuid:
                  type: string
                  format: uuid
                  description: Pass an empty string to explicitly clear the channel association.
                enabled: {type: boolean}
                tool_instance_ids:
                  type: array
                  items: {type: integer}
                  description: When present, replaces the cron-agent tool allowlist. Omit to leave existing tools untouched; pass `[]` to clear.
      responses:
        '200':
          description: Updated cron job
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CronJob'}
        '400':
          description: Validation error (bad schedule, unknown field, missing channel, ...)
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Duplicate name conflict.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '503':
          description: Cron service not configured
    delete:
      summary: Delete cron job
      operationId: deleteCronJob
      tags: [Cron Jobs]
      responses:
        '204':
          description: Cron job deleted
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            System cron job (`is_system=true`) cannot be deleted. Operators can
            disable it instead via PUT.
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}
        '503':
          description: Cron service not configured

  /cron-jobs/{uuid}/run:
    parameters:
      - name: uuid
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      summary: Manually trigger a cron job
      operationId: runCronJobNow
      tags: [Cron Jobs]
      responses:
        '202':
          description: Job tick accepted and running in the background. Poll the cron job row's last_run_status/last_run_error for the outcome.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, example: started}
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: Cron service not configured

  /alert-source-types:
    get:
      summary: List alert source types
      operationId: listAlertSourceTypes
      tags: [Alert Sources]
      responses:
        '200':
          description: Available alert source types
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AlertSourceType'

  /alert-sources:
    get:
      summary: List alert source instances
      operationId: listAlertSources
      tags: [Alert Sources]
      responses:
        '200':
          description: Configured alert sources
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/AlertSourceInstance'
    post:
      summary: Create alert source
      operationId: createAlertSource
      tags: [Alert Sources]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source_type_name, name]
              properties:
                source_type_name:
                  type: string
                name:
                  type: string
                description:
                  type: string
                webhook_secret:
                  type: string
                field_mappings:
                  type: object
                settings:
                  type: object
                notification_channel_uuid:
                  type: string
                  format: uuid
                  nullable: true
                  description: Optional UUID of a Channel to route outbound posts to. When omitted, the alert source falls back to the provider's default channel.
      responses:
        '201':
          description: Alert source created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertSourceInstance'

  /alert-sources/{uuid}:
    parameters:
      - name: uuid
        in: path
        required: true
        schema:
          type: string
    get:
      summary: Get alert source
      operationId: getAlertSource
      tags: [Alert Sources]
      responses:
        '200':
          description: Alert source details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertSourceInstance'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update alert source
      operationId: updateAlertSource
      tags: [Alert Sources]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                webhook_secret:
                  type: string
                field_mappings:
                  type: object
                settings:
                  type: object
                notification_channel_uuid:
                  type: string
                  format: uuid
                  nullable: true
                  description: Optional UUID of a Channel to route outbound posts to. Pass an empty string to clear the override.
                enabled:
                  type: boolean
      responses:
        '200':
          description: Updated alert source
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AlertSourceInstance'
    delete:
      summary: Delete alert source
      operationId: deleteAlertSource
      tags: [Alert Sources]
      responses:
        '204':
          description: Alert source deleted

  /memories:
    get:
      summary: List cross-incident memories with optional scope/type filters
      operationId: listMemories
      tags: [Memories]
      parameters:
        - name: scope
          in: query
          schema: {type: string}
          description: 'Filter by scope ("global" or skill name)'
        - name: type
          in: query
          schema: {type: string, enum: [host, incident_pattern, tool_quirk, feedback]}
      responses:
        '200':
          description: List of memories
          content:
            application/json:
              schema:
                type: array
                items: {$ref: '#/components/schemas/Memory'}
    post:
      summary: Create a new memory entry
      operationId: createMemory
      tags: [Memories]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/MemoryRequest'}
      responses:
        '201':
          description: Memory created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Memory'}
        '400':
          description: Validation error
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ErrorResponse'}

  /memories/scopes:
    get:
      summary: List distinct scopes that have memories
      operationId: listMemoryScopes
      tags: [Memories]
      responses:
        '200':
          description: Array of scope strings
          content:
            application/json:
              schema:
                type: array
                items: {type: string}

  /memories/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: integer}
    get:
      summary: Retrieve a single memory
      operationId: getMemory
      tags: [Memories]
      responses:
        '200':
          description: Memory record
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Memory'}
        '404':
          description: Not found
    put:
      summary: Update a memory
      operationId: updateMemory
      tags: [Memories]
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/MemoryRequest'}
      responses:
        '200':
          description: Updated memory
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Memory'}
    delete:
      summary: Delete a memory
      operationId: deleteMemory
      tags: [Memories]
      responses:
        '204':
          description: Memory deleted

  /incidents/{uuid}/feedback:
    parameters:
      - in: path
        name: uuid
        required: true
        schema: {type: string}
    post:
      summary: Persist operator feedback for an incident as a global memory entry
      operationId: createIncidentFeedback
      tags: [Memories]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:
                  type: string
                  description: Free-form feedback text from the operator
      responses:
        '201':
          description: Feedback stored as scope=global memory of type=feedback
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Memory'}

# ---------------------------------------------------------------------------
# Documentation-only overlay added when this document is published on
# akmatori.com. It describes the conventions the API follows; it does not
# change any operation, schema, or security scheme.
# ---------------------------------------------------------------------------
x-api-lifecycle:
  currentMajorVersion: 1
  versionSource: info.version
  versioningScheme: url-path
  description: >-
    The base path /api serves API major version 1. A breaking change is
    published as a new major version under a new path prefix (/api/v2); the
    previous major version keeps serving its existing path until its announced
    sunset date. Additive changes - new endpoints, new optional request fields,
    new response fields - ship inside the current major version and are not
    breaking. Clients must ignore response fields they do not recognise.
  deprecationPolicy:
    minimumNoticeDays: 90
    headers:
      - name: Deprecation
        specification: https://www.rfc-editor.org/rfc/rfc9745.html
        description: Set on responses from a deprecated endpoint or major version.
      - name: Sunset
        specification: https://www.rfc-editor.org/rfc/rfc8594.html
        description: Timestamp after which the deprecated surface stops responding.
      - name: Link
        rel: deprecation
        description: Points at the documentation for the replacement surface.
    documentation: https://akmatori.com/docs/api
  rateLimits:
    enforcedByDefault: false
    description: >-
      A self-hosted Akmatori deployment applies no request quota of its own; the
      effective limit is whatever gateway, ingress, or proxy fronts it. When a
      limit is enforced, Akmatori and its reference deployments use the standard
      RateLimit fields, and clients should treat their absence as "no advertised
      limit" rather than "unlimited".
    headers:
      - name: RateLimit
        specification: https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-09.html
        description: 'Combined quota state, e.g. "limit=100, remaining=42, reset=17".'
      - name: RateLimit-Policy
        description: The quota window that produced the RateLimit field.
      - name: Retry-After
        specification: https://www.rfc-editor.org/rfc/rfc9110.html#field.retry-after
        description: Seconds to wait before retrying. Always set on a 429 response.
    clientGuidance: >-
      On 429, wait for Retry-After (never retry sooner), then apply exponential
      backoff with jitter. Treat 429 as backpressure, not as an error to escalate.
