openapi: 3.0.3
info:
  title: SOAT Tags
  version: 1.0.0
  description: >-
    Shared components for the tag bag every tagged resource carries. This file
    declares no paths: it exists so the shape of a tag bag, and the `?tags=`
    filter that reads it, are defined once rather than once per module.
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
paths: {}
components:
  schemas:
    TagBag:
      type: object
      additionalProperties:
        type: string
      # The CLI flag a tag route's whole body arrives in: `--tags '{…}'`.
      x-cli-flag-name: tags
      description: >-
        Key-value labels on a resource. A flat object of string values —
        an array, a nested object or a number is rejected with
        `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored
        verbatim, so `cost_center` and `costCenter` are two different tags.
        Matched by JSONB containment wherever tags are read: the `?tags=`
        filter, knowledge search, and the IAM `soat:ResourceTag/<key>`
        condition.


        Keys beginning `system.` are reserved: the platform writes them to
        record which conversation, actor, agent and role a row came from, and a
        write naming one is refused with `400 RESERVED_TAG_KEY`. They are read
        and filtered like any other tag.


        The bag is bounded, because every pair reaches the IAM context of every
        access check on the resource: at most 50 keys, each key at most 128
        characters and each value at most 256. A write past a bound — including
        a merge that would grow the stored bag past the key count — is
        `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed.
        `system.*` keys are the platform's and do not count against the 50.
      example: { team: 'finance', env: 'prod' }
    NullableTagBag:
      type: object
      nullable: true
      additionalProperties:
        type: string
      description: >-
        A `TagBag` on a request field where `null` is meaningful: a
        full-replacement update that clears the bag. A record whose bag is
        unset leaves the field out.
      example: { team: 'finance', env: 'prod' }
  parameters:
    TagsQuery:
      name: tags
      in: query
      required: false
      description: >
        Filter by tag pairs, written `key:value` (split on the first
        colon, so a value may contain colons). Repeat the parameter for
        several pairs; **all** must be present with exactly that value.
      schema:
        type: array
        items:
          type: string
      style: form
      explode: true
      example:
        - 'env:prod'
