openapi: 3.1.0
info:
  title: Dukk Server — API REST
  version: "2.1.0"
  summary: |
    API completa do dukk-server (Axum) — 5 tenants de autenticação:
    público, SaaS (Clerk JWT), admin (staff), webhooks e headless (bearer token).
  description: |
    O servidor expõe ~150 endpoints REST + 2 WebSocket, organizados em 5 routers
    com gates de autenticação distintos. Páginas estáticas (landing, hub de docs,
    Swagger UI, ReDoc, login standalone) são servidas sem autenticação.

    ## Tenants (routers)

    | Router     | Auth                          | Quem usa                      |
    |------------|-------------------------------|-------------------------------|
    | `public`   | Nenhuma                       | Health, landing page, OAuth   |
    | `saas`     | Clerk JWT (`require_auth`)    | Frontend Vue (usuário logado) |
    | `admin`    | Clerk JWT + `is_staff=true`   | Painel admin (/admin/*)       |
    | `webhooks` | Clerk Svix signature          | Clerk, GitHub                 |
    | `headless` | Bearer token (`require_bearer`)| CLI/TUI local, VSCode ext    |

    ## Autenticação

    - **SaaS / Admin**: Header `Authorization: Bearer <clerk_jwt>`.
      WebSockets aceitam `?token=<jwt>` na query string.
    - **Headless**: Header `Authorization: Bearer <server_token>`.
      Token gerado em `~/.dukk/server-token` (migrado do legado `~/.elai/server-token`).
    - **Webhooks**: Assinatura Svix nos headers `svix-id` / `svix-signature`.

    ## Streaming

    - **SSE** (`text/event-stream`): `GET /v1/sessions/{id}/events`
    - **WebSocket**:
      - `GET /v1/sessions/{id}/browser/preview/ws` — frames JPEG do browser
      - `GET /v1/sessions/{id}/permissions/ws` — permissões em tempo real

    ## Erros

    JSON `{"error": "<mensagem>"}` com HTTP status padrão
    (400, 401, 403, 404, 409, 422, 500, 503).
  contact:
    name: Dukk
    url: https://dukk.com.br
  license:
    name: Proprietary

servers:
  # Relativo: resolve pela origem da própria doc (esta MESMA spec é servida em
  # produção E integração). Assim o "Try it out" bate no host correto sozinho.
  - url: /
    description: Este host (mesma origem da doc — produção ou integração)
  - url: https://api.dukk.com.br
    description: Produção
  - url: https://int.dukk.com.br
    description: Integração
  - url: http://127.0.0.1:8080
    description: Loopback local (dev)

security: []

tags:
  # ── Público ──
  - name: Health
    description: "Tenant: public. Sem auth. Saúde do servidor."
  - name: Static Pages
    description: "Tenant: public. Sem auth. Landing, hub de docs, Swagger UI, ReDoc, login standalone."
  - name: Invites (público)
    description: "Tenant: public. Sem auth. Validação de código de convite."
  - name: OpenCode
    description: "Tenant: public. Sem auth. Catálogo de modelos no formato OpenCode."
  # ── SaaS ──
  - name: Me / Perfil
    description: "Tenant: saas. Clerk JWT. Dados e quotas do usuário."
  - name: Conversations
    description: "Tenant: saas. Clerk JWT. CRUD de conversas com IA."
  - name: Artifacts
    description: "Tenant: saas. Clerk JWT. Arquivos gerados pelo agente."
  - name: PDF Generation
    description: "Tenant: saas. Clerk JWT. Geração de PDF sem modelo."
  - name: Uploads
    description: "Tenant: saas. Clerk JWT. Extração de conteúdo de uploads."
  - name: Sessions
    description: "Tenant: saas. Clerk JWT. Execução de chat + SSE + WebSocket."
  - name: Browser Preview
    description: "Tenant: saas. Clerk JWT. Preview do navegador Dukk Browser."
  - name: Sandboxes
    description: "Tenant: saas. Clerk JWT. Containers Docker por usuário."
  - name: Filesystem
    description: "Tenant: saas. Clerk JWT. File picker local (host FS)."
  - name: Learning
    description: "Tenant: saas. Clerk JWT. Memórias + skills + reviews."
  - name: Routines
    description: "Tenant: saas. Clerk JWT. Rotinas automatizadas (cron + webhooks)."
  - name: Kanban
    description: "Tenant: saas. Clerk JWT. Boards, cards, comentários, stage area de changes e SSE."
  - name: Connectors
    description: "Tenant: saas (+ callback público). Integrações OAuth."
  - name: Blocks
    description: "Tenant: saas. Clerk JWT. Blocos compartilhados (shared blocks), user-scoped."
  - name: Skill Store
    description: "Tenant: saas. Clerk JWT. Catálogo unificado de skills."
  - name: Welcome Shortcuts
    description: "Tenant: saas. Clerk JWT. Atalhos da tela de boas-vindas."
  # ── Webhooks ──
  - name: Webhooks
    description: "Tenant: webhooks. Svix. Inbound de Clerk + GitHub."
  # ── Admin ──
  - name: Admin Ping
    description: "Tenant: admin. Clerk JWT + is_staff. Smoke test."
  - name: Admin Invites
    description: "Tenant: admin. Clerk JWT + is_staff. Gestão de convites."
  - name: Admin Waitlist
    description: "Tenant: admin. Clerk JWT + is_staff. Aprovação de waitlist."
  - name: Admin Users
    description: "Tenant: admin. Clerk JWT + is_staff. Listagem/edição de usuários."
  - name: Admin Logs
    description: "Tenant: admin. Clerk JWT + is_staff. Logs do sistema."
  - name: Admin Deep Research
    description: "Tenant: admin. Clerk JWT + is_staff. Configuração de modelo para deep research."
  - name: Admin VPS Env
    description: "Tenant: admin. Clerk JWT + is_staff. Variáveis de ambiente do serviço VPS."
  - name: Admin Skill Store
    description: "Tenant: admin. Clerk JWT + is_staff. Fila de aprovação + resync."
  - name: Admin Welcome Shortcuts
    description: "Tenant: admin. Clerk JWT + is_staff. CRUD de atalhos."
  # ── Headless ──
  - name: Version
    description: "Tenant: headless. Bearer token. Versão do servidor."
  - name: Workspace (Headless)
    description: "Tenant: headless. Bearer token. Operações de arquivo por sessão."
  - name: Git (Headless)
    description: "Tenant: headless. Bearer token. Comandos Git no workspace."
  - name: Session Ops (Headless)
    description: "Tenant: headless. Bearer token. Clone, compact, export, resume."
  - name: Tasks (Headless)
    description: "Tenant: headless. Bearer token. Tarefas em background."
  - name: Models & Providers
    description: "Tenant: headless. Bearer token. Catálogo de modelos e providers."
  - name: Commands (Headless)
    description: "Tenant: headless. Bearer token. Slash-commands + sessão."
  - name: Tools (Headless)
    description: "Tenant: headless. Bearer token. Tools + allow/deny + rate-limit."
  - name: Telemetry (Headless)
    description: "Tenant: headless. Bearer token. Eventos e uso."
  - name: Cache (Headless)
    description: "Tenant: headless. Bearer token. Estatísticas e limpeza."
  - name: MCP (Headless)
    description: "Tenant: headless. Bearer token. Servidores MCP."
  - name: Plugins (Headless)
    description: "Tenant: headless. Bearer token. Plugins + skills + agents + hooks."
  - name: User Commands (Headless)
    description: "Tenant: headless. Bearer token. Slash-commands customizadas."
  - name: Auth (Headless)
    description: "Tenant: headless. Bearer token. API keys + OAuth + import."
  - name: Config (Headless)
    description: "Tenant: headless. Bearer token. Config + budget + tema."

components:
  securitySchemes:
    clerkJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Clerk session JWT — frontend Vue (usuário logado)
    serverToken:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: Server token em ~/.dukk/server-token — CLI/TUI

  parameters:
    SessionId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID da sessão
    WorkspaceSessionId:
      in: path
      name: session_id
      required: true
      schema: { type: string }
      description: UUID da sessão dona do workspace
    TurnId:
      in: path
      name: turn_id
      required: true
      schema: { type: string }
      description: UUID do turno
    ConversationId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID da conversa
    ArtifactId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID do artifact
    SandboxId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID do sandbox
    RoutineId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID da rotina
    AppId:
      in: path
      name: app_id
      required: true
      schema: { type: string }
      description: "ID do conector (ex: github, slack)"
    ConnectorAction:
      in: path
      name: action
      required: true
      schema: { type: string }
      description: Nome da ação do conector
    SkillId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID da skill
    SkillName:
      in: path
      name: name
      required: true
      schema: { type: string }
      description: Nome da skill
    MemoryIndex:
      in: path
      name: index
      required: true
      schema: { type: integer }
      description: Índice da memória (0-based)
    ProviderId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: ID do provider LLM
    McpServerName:
      in: path
      name: name
      required: true
      schema: { type: string }
      description: Nome do servidor MCP
    McpToolName:
      in: path
      name: tool
      required: true
      schema: { type: string }
      description: Nome da tool MCP
    PluginName:
      in: path
      name: name
      required: true
      schema: { type: string }
      description: Nome do plugin
    AgentName:
      in: path
      name: name
      required: true
      schema: { type: string }
      description: Nome do agente
    UserCommandName:
      in: path
      name: name
      required: true
      schema: { type: string }
      description: Nome do comando
    AuthProvider:
      in: path
      name: provider
      required: true
      schema: { type: string }
      description: "Provider (ex: anthropic, openai)"
    TaskId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: UUID da task
    PermissionRequestId:
      in: path
      name: request_id
      required: true
      schema: { type: string }
      description: UUID da request de permissão
    AdminUserId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
      description: UUID do usuário
    WaitlistEntryId:
      in: path
      name: id
      required: true
      schema: { type: string }
      description: ID da entry no waitlist do Clerk
    InviteCode:
      in: path
      name: code
      required: true
      schema: { type: string }
      description: Código do convite
    WelcomeShortcutId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
      description: UUID do atalho
    KanbanBoardId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
      description: UUID do board do Kanban
    KanbanCardId:
      in: path
      name: id
      required: true
      schema: { type: string, format: uuid }
      description: UUID do card do Kanban

  responses:
    Ok:
      description: OK
    Created:
      description: Criado
    NoContent:
      description: Sem conteúdo (204)
    BadRequest:
      description: Parâmetros inválidos
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    Unauthorized:
      description: Não autenticado
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    Forbidden:
      description: Sem permissão (não-staff)
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    NotFound:
      description: Recurso não encontrado
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    Conflict:
      description: Conflito (recurso já existe)
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    InternalError:
      description: Erro interno
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
    ServiceUnavailable:
      description: Feature desabilitada
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }

  schemas:
    # ─── Conversations ────────────────────────────────────────────────────
    Conversation:
      type: object
      description: |
        Struct minimalista de conversation (sem agregações).
        Retornado por `POST /v1/conversations` e `GET /v1/conversations/{id}`.
      required: [id, user_id, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        title: { type: string, nullable: true }
        model: { type: string, nullable: true }
        environment_type: { type: string, nullable: true }
        cwd: { type: string, nullable: true }
        sandbox_id: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ConversationMetadata:
      type: object
      description: |
        Shape rico devolvido por `GET /v1/conversations`. Estende `Conversation`
        com tudo que o desktop Dukk precisa para popular `ServerAIConversationMetadata`
        num único round-trip.
      required:
        [id, user_id, created_at, updated_at, creator, usage, message_count, artifact_ids, harness, permissions]
      properties:
        id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        title: { type: string, nullable: true }
        model: { type: string, nullable: true }
        environment_type: { type: string, nullable: true }
        cwd: { type: string, nullable: true }
        sandbox_id: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        creator:
          type: object
          required: [clerk_id, email]
          properties:
            clerk_id: { type: string }
            email: { type: string }
            name: { type: string, nullable: true }
            avatar_url: { type: string, nullable: true }
        sandbox:
          type: object
          nullable: true
          required: [id, name, image, running, is_default]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            image: { type: string }
            running: { type: boolean }
            is_default: { type: boolean }
        usage:
          type: object
          description: Soma dos campos de usage das messages JSONB.
          required: [input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens]
          properties:
            input_tokens: { type: integer }
            output_tokens: { type: integer }
            cache_creation_input_tokens: { type: integer }
            cache_read_input_tokens: { type: integer }
        message_count: { type: integer }
        artifact_ids:
          type: array
          items: { type: string, format: uuid }
        harness:
          type: string
          enum: [dukk-cloud, dukk-local]
          description: Engine que rodou a conversation (cloud com sandbox vs local host FS).
        permissions:
          type: object
          description: Stub enquanto sharing real não existe.
          properties:
            visibility: { type: string }

    # ─── Kanban ───────────────────────────────────────────────────────────
    KanbanBoard:
      type: object
      required: [id, user_id, name, created_at]
      properties:
        id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        name: { type: string }
        created_at: { type: string, format: date-time }

    KanbanCard:
      type: object
      required: [id, board_id, title, body, status, priority, payload, consecutive_failures, max_retries, max_runtime_seconds, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        board_id: { type: string, format: uuid }
        parent_card_id: { type: string, format: uuid, nullable: true }
        title: { type: string }
        body: { type: string }
        status:
          type: string
          enum: [triage, todo, doing, blocked, done, archived]
        priority: { type: integer }
        subagent_type: { type: string, nullable: true }
        model: { type: string, nullable: true }
        assignee: { type: string, nullable: true }
        payload: { type: object }
        consecutive_failures: { type: integer }
        max_retries: { type: integer }
        max_runtime_seconds: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }

    KanbanComment:
      type: object
      required: [id, card_id, author, body, created_at]
      properties:
        id: { type: string, format: uuid }
        card_id: { type: string, format: uuid }
        author: { type: string }
        body: { type: string }
        created_at: { type: string, format: date-time }

paths:
  # ═══════════════════════════════════════════════════════════════════════════
  # TENANT: PUBLIC (sem auth)
  # ═══════════════════════════════════════════════════════════════════════════

  /v1/health:
    get:
      tags: [Health]
      summary: Healthcheck público
      operationId: health
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string }

  /v1/opencode/models:
    get:
      tags: [OpenCode]
      summary: Catálogo de modelos no formato OpenCode
      description: |
        Catálogo público de modelos LLM disponíveis no Dukk, formatado
        no shape esperado por integrações OpenCode (sem auth).
      operationId: listOpencodeModels
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object

  /v1/invites/validate:
    post:
      tags: [Invites (público)]
      summary: Validar código de convite (landing page)
      operationId: validateInvite
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200":
          description: Válido
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid: { type: boolean }
                  description: { type: string }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/routines/webhooks/github:
    post:
      tags: [Webhooks]
      summary: Webhook do GitHub para triggers de rotinas
      operationId: githubRoutineWebhook
      security: []
      responses:
        "200": { $ref: "#/components/responses/Ok" }

  /v1/connectors/{app_id}/callback:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    get:
      tags: [Connectors]
      summary: Callback OAuth de conector (provider externo redireciona sem JWT)
      description: |
        O `state` assinado por HMAC autentica o fluxo.
        Redireciona para o frontend após OAuth.
      operationId: connectorCallback
      security: []
      responses:
        "302":
          description: Redirecionamento para o frontend

  /webhooks/clerk:
    post:
      tags: [Webhooks]
      summary: Webhook do Clerk (user.created, user.updated, etc.)
      operationId: clerkWebhook
      security: []
      responses:
        "200": { $ref: "#/components/responses/Ok" }

  # ── Páginas estáticas (Static Pages) ──
  /:
    get:
      tags: [Static Pages]
      summary: Landing page do servidor Dukk
      description: Página inicial em HTML servida sem autenticação.
      operationId: staticIndex
      security: []
      responses:
        "200":
          description: HTML da landing
          content:
            text/html:
              schema: { type: string }

  /docs:
    get:
      tags: [Static Pages]
      summary: Hub de documentação da API
      description: |
        Página com cards para Swagger UI, ReDoc, OpenAPI YAML e categorias da API.
        Usa iframes apontando para `/swagger` e `/redoc`.
      operationId: staticDocsHub
      security: []
      responses:
        "200":
          description: HTML do hub
          content:
            text/html:
              schema: { type: string }

  /swagger:
    get:
      tags: [Static Pages]
      summary: Swagger UI (try-it interativo)
      operationId: staticSwagger
      security: []
      responses:
        "200":
          description: Página Swagger UI
          content:
            text/html:
              schema: { type: string }

  /redoc:
    get:
      tags: [Static Pages]
      summary: ReDoc (referência três colunas)
      operationId: staticRedoc
      security: []
      responses:
        "200":
          description: Página ReDoc
          content:
            text/html:
              schema: { type: string }

  /openapi.yaml:
    get:
      tags: [Static Pages]
      summary: Spec OpenAPI 3.1 em YAML
      description: Arquivo-fonte servido pelo próprio servidor — consumido por Swagger/ReDoc.
      operationId: staticOpenapiYaml
      security: []
      responses:
        "200":
          description: YAML
          content:
            application/yaml:
              schema: { type: string }
            text/yaml:
              schema: { type: string }

  /login:
    get:
      tags: [Static Pages]
      summary: Página de login standalone (desktop OAuth)
      description: |
        Fluxo OAuth standalone para o app desktop — isolado do front-web Vue
        (que tem race conditions com global guard + Clerk `<SignIn>` widget).
      operationId: staticLogin
      security: []
      responses:
        "200":
          description: HTML da página de login
          content:
            text/html:
              schema: { type: string }

  /empresas/login:
    get:
      tags: [Static Pages]
      summary: Página de login enterprise standalone (desktop OAuth)
      description: |
        Variante visual enterprise da página de login standalone do desktop.
        Compartilha CSS + JS em `/assets/login/` com `/login`.
      operationId: staticEmpresasLogin
      security: []
      responses:
        "200":
          description: HTML da página de login enterprise
          content:
            text/html:
              schema: { type: string }

  # ═══════════════════════════════════════════════════════════════════════════
  # TENANT: SAAS (Clerk JWT — require_auth)
  # ═══════════════════════════════════════════════════════════════════════════

  # ── Me / Perfil ──
  /v1/me:
    get:
      tags: [Me / Perfil]
      summary: Dados do usuário logado
      operationId: getMe
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  email: { type: string }
                  is_staff: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Me / Perfil]
      summary: Atualizar perfil
      operationId: patchMe
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/me/usage:
    get:
      tags: [Me / Perfil]
      summary: Uso de quotas do usuário (H5 / diário / mensal)
      operationId: getMyUsage
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  h5_used: { type: integer }
                  h5_limit: { type: integer }
                  daily_used: { type: integer }
                  daily_limit: { type: integer }
                  monthly_used: { type: integer }
                  monthly_limit: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/invites/redeem:
    post:
      tags: [Me / Perfil]
      summary: Resgatar código de convite
      operationId: redeemInvite
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Conversations ──
  /v1/conversations:
    post:
      tags: [Conversations]
      summary: Criar conversa
      description: |
        Cria nova conversa. O modelo ativo é resolvido server-side
        (via `db::config::get_active_model`); o cliente apenas passa o título opcional.
      operationId: createConversation
      security: [{ clerkJwt: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string, nullable: true }
      responses:
        "201":
          description: Conversa criada (shape `Conversation` minimalista)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Conversation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Conversations]
      summary: Listar conversas (shape rico ConversationMetadata)
      description: |
        Retorna um array de `ConversationMetadata` — Conversation crua + creator
        (JOIN users), sandbox (LEFT JOIN sandboxes), usage agregado (SUM messages.usage),
        artifact_ids (array_agg), message_count e harness derivado (`dukk-cloud` / `dukk-local`).

        Aceita `?ids=<csv-uuid>` opcional para filtrar por lista de conversation_ids.
      operationId: listConversations
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: ids
          schema: { type: string }
          description: Lista CSV de UUIDs de conversation para filtrar.
      responses:
        "200":
          description: Lista de conversations com metadata rico
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ConversationMetadata" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/conversations/{id}:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    get:
      tags: [Conversations]
      summary: Detalhes da conversa (shape minimalista)
      description: |
        Retorna o struct `Conversation` cru (sem metadata enriquecido).
        Use `GET /v1/conversations` para o shape rico.
      operationId: getConversation
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Conversation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Conversations]
      summary: Deletar conversa
      operationId: deleteConversation
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/conversations/{id}/fork:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    post:
      tags: [Conversations]
      summary: Fork de conversa (duplica conversa + mensagens)
      description: |
        Duplica a conversa (com suas mensagens) num novo id do mesmo usuário.
        Usado pelo handoff local→cloud do desktop Dukk.
      operationId: forkConversation
      security: [{ clerkJwt: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string, description: "Título da conversa forkada (opcional)." }
      responses:
        "201":
          description: Conversa forkada
          content:
            application/json:
              schema:
                type: object
                required: [forked_conversation_id]
                properties:
                  forked_conversation_id: { type: string, format: uuid }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/agent-runs:
    get:
      tags: [Conversations]
      summary: Listar agent runs / cloud agents do usuário
      description: |
        Substitui o legado `GET /api/v1/agent/runs` da Warp cloud. Fonte
        híbrida: conversas cloud (sandbox) + runs de kanban, deduplicadas
        por `conversation_id`, ordenadas por `updated_at DESC`.
      operationId: listAgentRuns
      security: [{ clerkJwt: [] }]
      parameters:
        - { name: limit, in: query, required: false, schema: { type: integer }, description: "Limite de runs (default 100)." }
        - { name: state, in: query, required: false, schema: { type: string }, description: "CSV de estados wire (QUEUED,INPROGRESS,…). Vazio = todos." }
        - { name: source, in: query, required: false, schema: { type: string }, description: "Filtra por source (ex.: CLOUD_MODE, SCHEDULED_AGENT)." }
        - { name: created_after, in: query, required: false, schema: { type: string, format: date-time }, description: "Mantém runs com created_at após este instante (RFC3339)." }
        - { name: environment_id, in: query, required: false, schema: { type: string }, description: "Filtra por id do environment (sandbox)." }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/conversations/{id}/messages:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    get:
      tags: [Conversations]
      summary: Mensagens da conversa
      operationId: getMessages
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/conversations/{id}/artifacts:
    parameters: [{ $ref: "#/components/parameters/ConversationId" }]
    get:
      tags: [Artifacts]
      summary: Artifacts vinculados à conversa
      operationId: listConversationArtifacts
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Artifacts ──
  /v1/artifacts:
    get:
      tags: [Artifacts]
      summary: Listar todos os artifacts do usuário
      operationId: listArtifacts
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/artifacts/{id}:
    parameters: [{ $ref: "#/components/parameters/ArtifactId" }]
    get:
      tags: [Artifacts]
      summary: Conteúdo do artifact (binário ou texto)
      operationId: getArtifactContent
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: Conteúdo do artifact
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Artifacts]
      summary: Deletar artifact
      operationId: deleteArtifact
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/artifacts/{id}/skill:
    parameters: [{ $ref: "#/components/parameters/ArtifactId" }]
    get:
      tags: [Artifacts]
      summary: Skill associada ao artifact
      operationId: getArtifactSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/artifacts/{id}/metadata:
    parameters: [{ $ref: "#/components/parameters/ArtifactId" }]
    get:
      tags: [Artifacts]
      summary: Metadata do artifact
      operationId: getArtifactMetadata
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/artifacts/{id}/preview:
    parameters: [{ $ref: "#/components/parameters/ArtifactId" }]
    get:
      tags: [Artifacts]
      summary: Preview do artifact (thumbnail/render)
      operationId: getArtifactPreview
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: Preview (imagem ou HTML)
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── PDF Generation ──
  /v1/tools/pdf_generate:
    post:
      tags: [PDF Generation]
      summary: Gerar PDF via HTTP (acionado pelo botão "Exportar PDF" do front)
      description: |
        Não passa pelo loop do modelo. Persiste como artifact
        com preview_status='skipped' (PDF é nativo).
      operationId: generatePdf
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                markdown: { type: string }
                title: { type: string }
      responses:
        "201":
          description: PDF gerado como artifact
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Uploads ──
  /v1/uploads/extract:
    post:
      tags: [Uploads]
      summary: Extrair conteúdo de upload (PDF, imagem, DOCX, etc.)
      operationId: extractUpload
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Conteúdo extraído
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/uploads/extract:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Uploads]
      summary: Extrair upload no contexto da sessão (anexa ao histórico)
      description: |
        Igual a `/v1/uploads/extract`, porém associa o resultado à sessão
        para que o agente tenha acesso ao conteúdo no próximo turno.
      operationId: extractUploadForSession
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Conteúdo extraído e associado à sessão
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Sessions ──
  /v1/sessions:
    post:
      tags: [Sessions]
      summary: Criar sessão de chat
      operationId: createSession
      security: [{ clerkJwt: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                conversation_id: { type: string, format: uuid }
                model: { type: string }
                permission_mode: { type: string }
      responses:
        "201":
          description: Sessão criada
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Sessions]
      summary: Listar sessões do usuário
      operationId: listSessions
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: Obter sessão com mensagens
      operationId: getSession
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Sessions]
      summary: Deletar sessão
      operationId: deleteSession
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Sessions]
      summary: Atualizar modelo / permission_mode
      operationId: patchSession
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                model: { type: string }
                permission_mode: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/messages:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Sessions]
      summary: Enviar mensagem e iniciar (ou enfileirar) um turno do agente
      description: |
        Envia um prompt do usuário para a sessão. Se nenhum turno está em
        execução, inicia um novo turno imediatamente; se já há um turno
        ativo, a mensagem entra na fila FIFO da sessão (drain automático no
        fim do turno corrente) e a resposta vem com status `202`.

        Os resultados (texto, tool calls, etc.) chegam de forma assíncrona
        via SSE em `GET /v1/sessions/{id}/events`. A resposta deste POST traz
        apenas o `turn_id` para correlação/cancelamento.

        Headers opcionais (modo desktop/local):
        - `X-Dukk-Client-Exec: bash,read_file,...` — anuncia as tools de
          filesystem que o cliente vai executar localmente; o backend então
          delega via evento `tool_delegated_to_client`.
        - `X-Dukk-Cloud-Authorization: Bearer <token>` — bearer cloud
          repassado por-turn quando este processo é um sidecar local, usado
          para delegar tools que exigem a infra cloud.
      operationId: sendMessage
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  description: Texto do prompt do usuário.
                attachments:
                  type: array
                  description: |
                    Anexos já extraídos (via `POST /v1/sessions/{id}/uploads/extract`
                    ou `POST /v1/uploads/extract`) a injetar no input do turno.
                  items:
                    type: object
                    required: [filename, mime_type, size, text, truncated]
                    properties:
                      filename: { type: string }
                      mime_type: { type: string }
                      size: { type: integer, format: int64 }
                      text:
                        type: string
                        description: Texto extraído do arquivo.
                      truncated:
                        type: boolean
                        description: true se o texto foi truncado por limite de tamanho.
                      sandbox_path:
                        type: string
                        nullable: true
                        description: |
                          Path absoluto no guest (`/workspace/Arquivos/<filename>`)
                          quando o upload foi persistido no sandbox. Ausente para o
                          endpoint stateless.
      responses:
        "200":
          description: Turno iniciado (stream via SSE em /events).
          content:
            application/json:
              schema:
                type: object
                required: [turn_id]
                properties:
                  turn_id:
                    type: string
                    description: ULID do turno iniciado.
        "202":
          description: |
            Mensagem enfileirada (já havia um turno ativo). `turn_id` vem no
            formato `queued:<posição>` (1 = próxima a sair). Também emite o
            evento SSE `message_queued`.
          content:
            application/json:
              schema:
                type: object
                required: [turn_id]
                properties:
                  turn_id:
                    type: string
                    example: "queued:1"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Cota excedida (limite de uso do plano atingido).
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/turns/{turn_id}/cancel:
    parameters:
      - { $ref: "#/components/parameters/SessionId" }
      - { $ref: "#/components/parameters/TurnId" }
    post:
      tags: [Sessions]
      summary: Cancelar turno em execução
      operationId: cancelTurn
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/events:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: Stream SSE de eventos da sessão (text/event-stream)
      description: |
        Cada evento SSE tem um `event:` (nome em snake_case) e um `data:`
        com JSON serializado do enum `ServerEvent` (`tag = "type"`,
        `rename_all = "snake_case"`). Todo evento carrega `seq` (monotônico
        por sessão; use `?since=N` para reconectar) e `session_id`.

        Aceita `?since=<seq>` na query para reenviar apenas eventos com
        `seq > since` (reconexão sem perda).

        Eventos emitidos (35 variantes do `ServerEvent`):

        - `snapshot` — estado inicial da sessão (`session: SessionSnapshot`).
        - `turn_started` / `turn_completed` / `turn_error` / `turn_cancelled`
          — ciclo de vida do turno (`turn_id`).
        - `text_delta` (`text`) — chunk de texto do assistente.
        - `thinking_delta` (`thinking`) — chunk de raciocínio (extended thinking).
        - `tool_use_started` (`tool_call_id`, `tool_name`).
        - `tool_use_input_delta` (`tool_call_id`, `partial_json`).
        - `tool_result` (`tool_call_id`, `output`, `is_error`) — terminal.
        - `tool_progress` (`tool_call_id`, `message`) — progresso incremental
          de tools longas (ex.: DeepResearch); pode repetir.
        - `tool_delegated_to_client` (`tool_call_id`, `tool_name`, `input`)
          — pede ao cliente local executar a tool e devolver via
          `POST /v1/sessions/{id}/tools/{tool_call_id}/result`. Bloqueia o turno.
        - `permission_request` (`request_id`, `tool_name`, `input`,
          `required_mode`) — responda em `POST /v1/permissions/{request_id}/decide`.
        - `usage_delta` (`input_tokens`, `output_tokens`).
        - `message_appended` (`role`, `text_summary`).
        - `message_queued` (`queue_position`) — mensagem enfileirada (FIFO).
        - `sandbox_boot_started` (`sandbox_id`, `image`),
          `sandbox_boot_progress` (`elapsed_seconds`, `message`),
          `sandbox_ready` (`sandbox_id`),
          `sandbox_boot_failed` (`sandbox_id`, `error`).
        - `file_created` (`path`, `size`, `mime?`, `tool_call_id?`,
          `message_id?`, `message_file_id?`).
        - `artifact_created` (`artifact_id`, `name`, `mime`, `size`,
          `preview_status`), `artifact_preview_ready` (`artifact_id`),
          `artifact_preview_failed` (`artifact_id`, `error`),
          `artifact_open_requested` (`source: PreviewSource`).
        - `questions_emitted` (`tool_call_id`, `questions[]`) — bloqueia o turno;
          responda em `POST /v1/sessions/{id}/questions/answer`.
        - `questions_answered` (`tool_call_id`, `answers`, `skipped[]`).
        - `plan_suggested` (`tool_call_id`, `summary`, `proposed_tasks[]`) —
          bloqueia o turno; decida em `POST /v1/sessions/{id}/plan/decide`.
        - `plan_decided` (`tool_call_id`, `decision`).
        - `todo_updated` (`tool_call_id`, `todos[]`) — fire-and-forget.
        - `background_review_started` (`task_id`, `label`, `review_memory`,
          `review_skills`), `background_review_completed` (`task_id`,
          `status`, `summary?`).
        - `browser_setup_progress` (`stage`, `message`, `percent`),
          `browser_setup_failed` (`stage`, `error`).
      operationId: streamEvents
      security: [{ clerkJwt: [] }]
      parameters:
        - name: since
          in: query
          required: false
          schema: { type: integer, format: int64 }
          description: Reenvia apenas eventos com `seq` maior que este valor.
      responses:
        "200":
          description: Stream SSE
          content:
            text/event-stream:
              schema:
                type: string
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/cost:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: Custo total da sessão (USD + tokens)
      operationId: getSessionCost
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/context:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: Contexto atual da sessão (system prompt + histórico)
      operationId: getSessionContext
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/permissions/pending:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: Permissões pendentes de aprovação
      operationId: listPendingPermissions
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/permissions/{request_id}/decide:
    parameters: [{ $ref: "#/components/parameters/PermissionRequestId" }]
    post:
      tags: [Sessions]
      summary: Aprovar ou rejeitar permissão
      operationId: decidePermission
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [allow, deny]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/questions/answer:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Sessions]
      summary: Responder perguntas do agente
      description: |
        Frontend POSTa as respostas das perguntas emitidas pela tool
        `ask_user_questions`; o questions_bridge libera o `oneshot` e o
        turno do LLM retoma sem reiniciar.
      operationId: answerQuestions
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/plan/decide:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Sessions]
      summary: Aprovar ou ajustar plano proposto pelo agente
      description: |
        Decide um plano emitido pelo agente em plan mode (plan_bridge).
        Análogo a `/permissions/{id}/decide` mas para planos estruturados.
      operationId: decidePlan
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decision]
              properties:
                decision:
                  type: string
                  enum: [approve, reject, edit]
                edits: { type: object }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/tools/{tool_call_id}/result:
    parameters:
      - { $ref: "#/components/parameters/SessionId" }
      - name: tool_call_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Sessions]
      summary: Devolver resultado de tool delegada ao cliente
      description: |
        Responde ao evento SSE `tool_delegated_to_client`: o cliente local
        (desktop, modo local) executa a tool de filesystem/cwd no ambiente
        dele e devolve o resultado aqui. O turno permanece bloqueado até este
        POST chegar.
      operationId: reportToolResult
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                output: { type: string }
                is_error: { type: boolean }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Browser Preview (Dukk Browser) ──
  /v1/sessions/{id}/browser/preview/start:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Browser Preview]
      summary: Ativar streaming de preview do navegador
      description: |
        Habilita captura automática de screenshots após
        ferramentas DOM-mutantes (browser_navigate, browser_click, etc.).
        Idempotente.
      operationId: startBrowserPreview
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/sessions/{id}/browser/preview/stop:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Browser Preview]
      summary: Desativar streaming de preview
      description: |
        Limpa o subscriber set. WebSocket aberto fecha no próximo recv.
        Idempotente.
      operationId: stopBrowserPreview
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/browser/preview/ws:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Browser Preview]
      summary: WebSocket — stream de frames JPEG do navegador
      description: |
        Aceita `?token=<jwt>` na query (browser WebSocket API não suporta
        headers customizados). Envia mensagens Text (meta JSON com URL)
        e Binary (JPEG, ~30-50 KB, q=50, 800px).
      operationId: browserPreviewWs
      security: [{ clerkJwt: [] }]
      responses:
        "101":
          description: WebSocket upgrade
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/sessions/{id}/browser/preview/frames:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Browser Preview]
      summary: Polling fallback de frames (alternativa ao WebSocket)
      description: |
        Devolve frames recentes do preview do navegador para clientes
        que não conseguem usar WebSocket. Aceita `?since=<timestamp>`
        opcional para incremental.
      operationId: browserPreviewFramesSince
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: since
          schema: { type: integer, format: int64 }
          description: Cursor (ms ou seq) para devolver só frames novos.
      responses:
        "200":
          description: Lista de frames disponíveis
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  # ── Sandboxes ──
  /v1/sandboxes:
    post:
      tags: [Sandboxes]
      summary: Criar sandbox (container Docker)
      operationId: createSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "201":
          description: Sandbox criado
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [Sandboxes]
      summary: Listar sandboxes do usuário
      operationId: listSandboxes
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sandboxes/default:
    get:
      tags: [Sandboxes]
      summary: Sandbox padrão do usuário (idempotente)
      description: |
        Chamado no primeiro login (lazy_create_user) e no mount do frontend.
        Garante que sempre há um ambiente pronto antes de renderizar a UI.
      operationId: getDefaultSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sandboxes/default/reset:
    post:
      tags: [Sandboxes]
      summary: Reset destrutivo do sandbox padrão
      description: |
        Apaga sandbox + volume + todas as conversations vinculadas,
        recria do zero. Front deve confirmar com modal antes de chamar.
      operationId: resetDefaultSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sandboxes/{id}:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    get:
      tags: [Sandboxes]
      summary: Detalhes do sandbox
      operationId: getSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Sandboxes]
      summary: Atualizar sandbox
      operationId: updateSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Sandboxes]
      summary: Deletar sandbox
      operationId: deleteSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/start:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    post:
      tags: [Sandboxes]
      summary: Iniciar sandbox
      operationId: startSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/stop:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    post:
      tags: [Sandboxes]
      summary: Parar sandbox
      operationId: stopSandbox
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/scratch/clear:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    post:
      tags: [Sandboxes]
      summary: Limpar estado transitório do sandbox (best-effort)
      description: |
        Stub no-op por enquanto (valida ownership e devolve 204).
        Front chama quando o usuário troca de ambiente.
      operationId: clearSandboxScratch
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/status:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    get:
      tags: [Sandboxes]
      summary: Status do sandbox
      operationId: sandboxStatus
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/files:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    get:
      tags: [Sandboxes]
      summary: Listar arquivos do sandbox
      operationId: listSandboxFiles
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/files/content:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    get:
      tags: [Sandboxes]
      summary: Conteúdo de arquivo do sandbox
      operationId: readSandboxFileContent
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sandboxes/{id}/files/raw:
    parameters: [{ $ref: "#/components/parameters/SandboxId" }]
    get:
      tags: [Sandboxes]
      summary: Arquivo raw do sandbox (binário)
      operationId: readSandboxFileRaw
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: Conteúdo binário
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Filesystem ──
  /v1/fs/list:
    get:
      tags: [Filesystem]
      summary: Listar diretório do host (file picker local)
      operationId: fsList
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/fs/read:
    get:
      tags: [Filesystem]
      summary: Ler arquivo do host
      operationId: fsRead
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/fs/home:
    get:
      tags: [Filesystem]
      summary: Caminho do home do host
      operationId: fsHome
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Learning ──
  /v1/learning/memory:
    get:
      tags: [Learning]
      summary: Listar memórias do usuário
      operationId: listMemory
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  general:
                    type: array
                    items: { type: string }
                  user:
                    type: array
                    items: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Learning]
      summary: Adicionar memória
      operationId: addMemory
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind, content]
              properties:
                kind:
                  type: string
                  enum: [general, user]
                content: { type: string }
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/memory/{index}:
    parameters: [{ $ref: "#/components/parameters/MemoryIndex" }]
    put:
      tags: [Learning]
      summary: Substituir memória
      operationId: replaceMemory
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [kind, content]
              properties:
                kind:
                  type: string
                  enum: [general, user]
                content: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    delete:
      tags: [Learning]
      summary: Deletar memória
      operationId: deleteMemory
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/skills:
    get:
      tags: [Learning]
      summary: Listar skills de aprendizado
      operationId: listLearningSkills
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/skills/{name}:
    parameters: [{ $ref: "#/components/parameters/SkillName" }]
    get:
      tags: [Learning]
      summary: Detalhe da skill de aprendizado
      operationId: getLearningSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/learning/skills/{name}/archive:
    parameters: [{ $ref: "#/components/parameters/SkillName" }]
    post:
      tags: [Learning]
      summary: Arquivar skill
      operationId: archiveSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/skills/{name}/pin:
    parameters: [{ $ref: "#/components/parameters/SkillName" }]
    post:
      tags: [Learning]
      summary: Pinar skill
      operationId: pinSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/skills/{name}/mark-stale:
    parameters: [{ $ref: "#/components/parameters/SkillName" }]
    post:
      tags: [Learning]
      summary: Marcar skill como stale
      operationId: markStaleSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/learning/background-reviews:
    get:
      tags: [Learning]
      summary: Background reviews do usuário
      operationId: listBackgroundReviews
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Routines ──
  /v1/routines:
    get:
      tags: [Routines]
      summary: Listar rotinas do usuário
      operationId: listRoutines
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Routines]
      summary: Criar rotina automatizada
      operationId: createRoutine
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/routines/{id}:
    parameters: [{ $ref: "#/components/parameters/RoutineId" }]
    get:
      tags: [Routines]
      summary: Detalhes da rotina
      operationId: getRoutine
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Routines]
      summary: Atualizar rotina
      operationId: updateRoutine
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Routines]
      summary: Deletar rotina
      operationId: deleteRoutine
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/routines/{id}/run-now:
    parameters: [{ $ref: "#/components/parameters/RoutineId" }]
    post:
      tags: [Routines]
      summary: Executar rotina agora
      operationId: runRoutineNow
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/routines/{id}/toggle:
    parameters: [{ $ref: "#/components/parameters/RoutineId" }]
    post:
      tags: [Routines]
      summary: Ligar/desligar rotina
      operationId: toggleRoutine
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/routines/{id}/api-trigger:
    parameters: [{ $ref: "#/components/parameters/RoutineId" }]
    post:
      tags: [Routines]
      summary: Disparar rotina via API
      operationId: apiTriggerRoutine
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Kanban ──
  /v1/kanban/boards:
    get:
      tags: [Kanban]
      summary: Listar boards do usuário
      operationId: listKanbanBoards
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  boards:
                    type: array
                    items: { $ref: "#/components/schemas/KanbanBoard" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Kanban]
      summary: Criar board
      operationId: createKanbanBoard
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
      responses:
        "201":
          description: Board criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KanbanBoard" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/kanban/boards/{id}:
    parameters: [{ $ref: "#/components/parameters/KanbanBoardId" }]
    delete:
      tags: [Kanban]
      summary: Deletar board (cascade nos cards)
      operationId: deleteKanbanBoard
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/boards/{id}/cards:
    parameters: [{ $ref: "#/components/parameters/KanbanBoardId" }]
    get:
      tags: [Kanban]
      summary: Listar cards do board (com filtro de status opcional)
      operationId: listKanbanCards
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum: [triage, todo, doing, blocked, done, archived]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  cards:
                    type: array
                    items: { $ref: "#/components/schemas/KanbanCard" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Kanban]
      summary: Enfileirar card no board
      description: |
        Cria card. `status` default é `triage`; `todo` envia direto pro pool
        de promoção. Suporta `idempotency_key` para evitar duplicação no retry.
      operationId: enqueueKanbanCard
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title: { type: string }
                body: { type: string, default: "" }
                subagent_type: { type: string, nullable: true }
                model: { type: string, nullable: true }
                priority: { type: integer, default: 0 }
                parent_card_id: { type: string, format: uuid, nullable: true }
                payload: { type: object }
                idempotency_key: { type: string, nullable: true }
                max_retries: { type: integer, nullable: true }
                max_runtime_seconds: { type: integer, nullable: true }
                status:
                  type: string
                  enum: [triage, todo]
                  default: triage
      responses:
        "201":
          description: Card criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KanbanCard" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    get:
      tags: [Kanban]
      summary: Detalhe do card
      operationId: getKanbanCard
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KanbanCard" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/move:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    post:
      tags: [Kanban]
      summary: Mover card para outro status
      operationId: moveKanbanCard
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [triage, todo, doing, blocked, done, archived]
      responses:
        "200":
          description: Card movido
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KanbanCard" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/cancel:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    post:
      tags: [Kanban]
      summary: Cancelar execução do card
      operationId: cancelKanbanCard
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/comments:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    post:
      tags: [Kanban]
      summary: Adicionar comentário ao card
      operationId: createKanbanComment
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }
      responses:
        "201":
          description: Comentário criado
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KanbanComment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/changes:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    get:
      tags: [Kanban]
      summary: Listar mudanças propostas (stage area do sub-agente)
      description: |
        F4 do Kanban — devolve a stage area das mudanças que o sub-agente
        Kanban propôs no card antes do usuário aplicar ou descartar.
      operationId: listKanbanCardChanges
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: Stage area de mudanças
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/changes/apply:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    post:
      tags: [Kanban]
      summary: Aplicar (parcialmente) mudanças propostas
      description: |
        Aceita listas separadas de change IDs a aceitar e rejeitar.
        Espelha a tool LLM `kanban_apply_card_changes`.
      operationId: applyKanbanCardChanges
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                accept:
                  type: array
                  items: { type: string }
                reject:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: Resultado da aplicação
          content:
            application/json:
              schema: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/cards/{id}/changes/discard:
    parameters: [{ $ref: "#/components/parameters/KanbanCardId" }]
    post:
      tags: [Kanban]
      summary: Descartar todas as mudanças propostas do card
      operationId: discardKanbanCardChanges
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/kanban/boards/{id}/events:
    parameters: [{ $ref: "#/components/parameters/KanbanBoardId" }]
    get:
      tags: [Kanban]
      summary: SSE de eventos do board (catch-up + live)
      description: |
        Stream `text/event-stream`. Aceita `?since=<event_id>` (BIGSERIAL)
        para replay incremental — reconnect sem perder eventos.
      operationId: streamKanbanBoardEvents
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: since
          schema: { type: integer, format: int64, default: 0 }
          description: Devolve eventos com id > since.
      responses:
        "200":
          description: Stream SSE
          content:
            text/event-stream:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Connectors ──
  /v1/connectors/catalog:
    get:
      tags: [Connectors]
      summary: Catálogo de conectores disponíveis
      operationId: listConnectorCatalog
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/connectors:
    get:
      tags: [Connectors]
      summary: Listar integrações do usuário
      operationId: listIntegrations
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/connectors/{app_id}/authorize:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    get:
      tags: [Connectors]
      summary: Iniciar fluxo OAuth do conector
      operationId: authorizeConnector
      security: [{ clerkJwt: [] }]
      responses:
        "302":
          description: Redireciona para o provider OAuth
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/connectors/{app_id}/device/start:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    get:
      tags: [Connectors]
      summary: "Iniciar device flow (ex.: GitHub)"
      description: |
        Device flow sem callback HTTP: o cliente exibe o `user_code` e polla
        em `/device/poll` até o usuário autorizar.
      operationId: connectorDeviceStart
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/connectors/{app_id}/device/poll:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    get:
      tags: [Connectors]
      summary: Polling do device flow
      description: |
        Verifica se o device flow já foi autorizado. O `state` (assinado por
        HMAC) carrega `device_code` + `user_id` + expiração.
      operationId: connectorDevicePoll
      security: [{ clerkJwt: [] }]
      parameters:
        - { name: state, in: query, required: true, schema: { type: string }, description: "State assinado retornado por /device/start." }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/connectors/{app_id}/connect:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    post:
      tags: [Connectors]
      summary: Conectar conector interno (identidade Clerk compartilhada)
      description: |
        Conector interno sem OAuth: valida o vínculo Clerk→usuário no serviço
        de destino e grava o opt-in.
      operationId: connectInternalConnector
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/connectors/{app_id}:
    parameters: [{ $ref: "#/components/parameters/AppId" }]
    delete:
      tags: [Connectors]
      summary: Remover integração
      operationId: deleteIntegration
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/connectors/{app_id}/action/{action}:
    parameters:
      - { $ref: "#/components/parameters/AppId" }
      - { $ref: "#/components/parameters/ConnectorAction" }
    post:
      tags: [Connectors]
      summary: Chamar ação do conector
      operationId: callConnectorAction
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Blocks ──
  /v1/blocks:
    get:
      tags: [Blocks]
      summary: Listar blocos compartilhados do usuário
      operationId: listBlocks
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Blocks]
      summary: Salvar bloco compartilhado e retornar URL pública
      operationId: saveBlock
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [show_prompt, display_setting, time_started_term]
              properties:
                command: { type: string }
                output: { type: string }
                stylized_command: { type: string }
                stylized_output: { type: string }
                stylized_prompt: { type: string }
                stylized_prompt_and_command: { type: string }
                pwd: { type: string }
                show_prompt: { type: boolean }
                display_setting: { type: string }
                title: { type: string }
                time_started_term: { type: string, format: date-time }
      responses:
        "200":
          description: Bloco salvo
          content:
            application/json:
              schema:
                type: object
                required: [id, url]
                properties:
                  id: { type: string }
                  url: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/blocks/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
    delete:
      tags: [Blocks]
      summary: Remover bloco do usuário
      operationId: deleteBlock
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Skill Store ──
  /v1/skill-store:
    get:
      tags: [Skill Store]
      summary: Listar skills do usuário (por scope)
      operationId: listSkills
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: scope
          schema:
            type: string
            enum: [user, installed, bundled, community, artifact]
          description: "Filtro de escopo (default: todas visíveis)"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  skills:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    post:
      tags: [Skill Store]
      summary: "Criar skill (scope: user ou artifact)"
      operationId: createSkill
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, content]
              properties:
                slug: { type: string }
                scope: { type: string, default: "user" }
                title: { type: string }
                description: { type: string }
                content: { type: string }
                priority: { type: integer, default: 50 }
                forked_from: { type: string }
      responses:
        "201":
          description: Skill criada
          content:
            application/json:
              schema: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/search:
    post:
      tags: [Skill Store]
      summary: Busca semântica de skills
      operationId: searchSkills
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query: { type: string }
                k: { type: integer, default: 8 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  skills:
                    type: array
                    items:
                      type: object
                      properties:
                        skill: { type: object }
                        score: { type: number }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/pinned:
    get:
      tags: [Skill Store]
      summary: Skills pinadas pelo usuário
      operationId: listPinnedSkills
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  skills:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/{id}:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    get:
      tags: [Skill Store]
      summary: Detalhe da skill
      operationId: getSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    put:
      tags: [Skill Store]
      summary: Atualizar conteúdo da skill
      operationId: updateSkill
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
    delete:
      tags: [Skill Store]
      summary: Deletar skill
      operationId: deleteSkill
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/{id}/pin:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    post:
      tags: [Skill Store]
      summary: Pinar skill
      operationId: pinSkillInStore
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Skill Store]
      summary: Despinar skill
      operationId: unpinSkillInStore
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/skill-store/{id}/publish:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    post:
      tags: [Skill Store]
      summary: Publicar skill para revisão da comunidade
      operationId: publishSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/skill-store/{id}/install:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    post:
      tags: [Skill Store]
      summary: Instalar skill da comunidade
      operationId: installSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/skill-store/import:
    post:
      tags: [Skill Store]
      summary: Importar skill de arquivo Markdown (SKILL.md)
      operationId: importSkill
      security: [{ clerkJwt: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/install-from-git:
    post:
      tags: [Skill Store]
      summary: Instalar skill direto de repositório Git
      description: |
        Clona repositório Git público com SKILL.md no root e
        registra a skill no escopo do usuário.
      operationId: installSkillFromGit
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, description: "URL do repo Git" }
                ref: { type: string, description: "Branch / tag (default: main)" }
                subdir: { type: string, description: "Subdiretório com SKILL.md" }
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/skill-store/refresh:
    post:
      tags: [Skill Store]
      summary: Refresh user-level dos syncs de skills (bundled + plugin + local)
      description: |
        Reexecuta os 3 syncs no escopo do usuário. Pensado para ser disparado
        pelo front depois do LLM criar um SKILL.md via tool, sem precisar de
        restart do server. Idempotente; mesmo handler do admin `resync` mas sem
        `require_staff`.
      operationId: refreshSkillStore
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Welcome Shortcuts ──
  /v1/welcome-shortcuts:
    get:
      tags: [Welcome Shortcuts]
      summary: Listar atalhos de boas-vindas (catálogo merged com prefs)
      operationId: listWelcomeShortcuts
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/welcome-shortcuts/prefs:
    put:
      tags: [Welcome Shortcuts]
      summary: Atualizar preferências de atalhos do usuário
      operationId: upsertWelcomeShortcutPrefs
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ═══════════════════════════════════════════════════════════════════════════
  # TENANT: ADMIN (Clerk JWT + is_staff=true)
  # ═══════════════════════════════════════════════════════════════════════════

  /v1/admin/ping:
    get:
      tags: [Admin Ping]
      summary: Smoke test do gate staff
      description: Retorna 200 com email do staff se o gate require_auth + require_staff funcionar.
      operationId: adminPing
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  staff_email: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/invites:
    get:
      tags: [Admin Invites]
      summary: Listar todos os códigos de convite
      operationId: adminListInvites
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin Invites]
      summary: Criar código de convite
      operationId: adminCreateInvite
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
                description: { type: string }
                max_uses: { type: integer }
                quota_h5: { type: integer, default: 100 }
                quota_daily: { type: integer, default: 250 }
                quota_monthly: { type: integer, default: 2500 }
                expires_at: { type: string, format: date-time }
      responses:
        "201":
          description: Convite criado
          content:
            application/json:
              schema: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/invites/{code}:
    parameters: [{ $ref: "#/components/parameters/InviteCode" }]
    get:
      tags: [Admin Invites]
      summary: Detalhes do convite + resgates + atividade por usuário
      operationId: adminGetInviteDetail
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  invite: { type: object }
                  seats_consumed: { type: integer }
                  seats_remaining: { type: integer }
                  redemptions:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Admin Invites]
      summary: Expirar (não deletar) código de convite
      operationId: adminExpireInvite
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/waitlist:
    get:
      tags: [Admin Waitlist]
      summary: Listar entries pendentes do waitlist (Clerk)
      operationId: adminListWaitlist
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "502": { $ref: "#/components/responses/InternalError" }

  /v1/admin/waitlist/{id}/approve:
    parameters: [{ $ref: "#/components/parameters/WaitlistEntryId" }]
    post:
      tags: [Admin Waitlist]
      summary: Aprovar entry do waitlist com vínculo a invite code
      operationId: adminApproveWaitlist
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invite_code]
              properties:
                invite_code: { type: string }
                redirect_base: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  redirect_url: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "502": { $ref: "#/components/responses/InternalError" }

  /v1/admin/users:
    get:
      tags: [Admin Users]
      summary: Listar usuários com filtros
      operationId: adminListUsers
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: q
          schema: { type: string }
          description: Busca textual
        - in: query
          name: plan
          schema: { type: string }
        - in: query
          name: is_staff
          schema: { type: boolean }
        - in: query
          name: limit
          schema: { type: integer, default: 200 }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/users/{id}:
    parameters: [{ $ref: "#/components/parameters/AdminUserId" }]
    patch:
      tags: [Admin Users]
      summary: Editar usuário (is_staff, quotas)
      operationId: adminPatchUser
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                is_staff: { type: boolean }
                quota_override_h5: { type: integer }
                quota_override_daily: { type: integer }
                quota_override_monthly: { type: integer }
                clear_overrides: { type: boolean }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/logs:
    get:
      tags: [Admin Logs]
      summary: Logs do sistema
      operationId: adminListLogs
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/deep-research/model:
    get:
      tags: [Admin Deep Research]
      summary: Modelo configurado para deep research (+ catálogo)
      operationId: adminGetDeepResearchModel
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: Configuração atual + opções disponíveis
          content:
            application/json:
              schema:
                type: object
                properties:
                  current_model: { type: string }
                  options:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Admin Deep Research]
      summary: Atualizar modelo de deep research
      operationId: adminSetDeepResearchModel
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model]
              properties:
                model: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/vps-env:
    get:
      tags: [Admin VPS Env]
      summary: Ler arquivo .env do serviço na VPS
      description: |
        Carrega o conteúdo de um .env de serviço (ex: dukk-server,
        dukk-browser) para edição pelo painel admin.
      operationId: adminGetVpsEnv
      security: [{ clerkJwt: [] }]
      parameters:
        - in: query
          name: file
          schema: { type: string }
          description: "Identificador do arquivo de env (ex: server, browser)."
      responses:
        "200":
          description: Conteúdo do .env + metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  content: { type: string }
                  info: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Admin VPS Env]
      summary: Atualizar arquivo .env (com restart de serviço opcional)
      operationId: adminPutVpsEnv
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                file: { type: string }
                content: { type: string }
                restart: { type: boolean, default: false }
      responses:
        "200":
          description: .env atualizado
          content:
            application/json:
              schema: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/skill-store/pending:
    get:
      tags: [Admin Skill Store]
      summary: Skills pendentes de aprovação
      operationId: adminListPendingSkills
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  skills:
                    type: array
                    items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/admin/skill-store/{id}/approve:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    post:
      tags: [Admin Skill Store]
      summary: Aprovar skill para publicação
      operationId: adminApproveSkill
      security: [{ clerkJwt: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/admin/skill-store/{id}/reject:
    parameters: [{ $ref: "#/components/parameters/SkillId" }]
    post:
      tags: [Admin Skill Store]
      summary: Rejeitar skill
      operationId: adminRejectSkill
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/admin/skill-store/resync:
    post:
      tags: [Admin Skill Store]
      summary: Re-sincronizar skills (bundled + plugin + local)
      description: |
        Incremental — não deleta nada. Útil após instalar
        plugin novo ou colocar SKILL.md em ~/.dukk/skills/.
      operationId: adminResyncSkillStore
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  bundled: { type: integer }
                  plugins: { type: integer }
                  local: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/admin/welcome-shortcuts:
    get:
      tags: [Admin Welcome Shortcuts]
      summary: Listar todos os atalhos (bruto, sem merge de prefs)
      operationId: adminListWelcomeShortcuts
      security: [{ clerkJwt: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin Welcome Shortcuts]
      summary: Criar atalho de boas-vindas
      operationId: adminCreateWelcomeShortcut
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label, prompt]
              properties:
                label: { type: string }
                prompt: { type: string }
                icon: { type: string }
      responses:
        "201":
          description: Atalho criado
          content:
            application/json:
              schema: { type: object }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/welcome-shortcuts/{id}:
    parameters: [{ $ref: "#/components/parameters/WelcomeShortcutId" }]
    delete:
      tags: [Admin Welcome Shortcuts]
      summary: Deletar atalho (cascade nas prefs dos usuários)
      operationId: adminDeleteWelcomeShortcut
      security: [{ clerkJwt: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ═══════════════════════════════════════════════════════════════════════════
  # TENANT: HEADLESS (Bearer token — require_bearer)
  # ═══════════════════════════════════════════════════════════════════════════

  /v1/version:
    get:
      tags: [Version]
      summary: Versão do servidor
      operationId: version
      security: [{ serverToken: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
                  name: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Permissions WebSocket ──
  /v1/sessions/{id}/permissions/ws:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    get:
      tags: [Sessions]
      summary: WebSocket — permissões em tempo real
      operationId: permissionsWs
      security: [{ serverToken: [] }]
      responses:
        "101":
          description: WebSocket upgrade
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Workspace ──
  /v1/workspace/{session_id}/read:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Workspace (Headless)]
      summary: Ler arquivo no workspace da sessão
      operationId: workspaceRead
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/workspace/{session_id}/write:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Workspace (Headless)]
      summary: Escrever arquivo no workspace
      operationId: workspaceWrite
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/edit:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Workspace (Headless)]
      summary: Editar arquivo (find & replace)
      operationId: workspaceEdit
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/glob:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Workspace (Headless)]
      summary: Buscar arquivos por glob
      operationId: workspaceGlob
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/grep:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Workspace (Headless)]
      summary: Buscar conteúdo com regex
      operationId: workspaceGrep
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/tree:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Workspace (Headless)]
      summary: Árvore de diretórios do workspace
      operationId: workspaceTree
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/diff:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Workspace (Headless)]
      summary: Diff do workspace (git diff)
      operationId: workspaceDiff
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Session Ops ──
  /v1/sessions/{id}/clone:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Session Ops (Headless)]
      summary: Clonar sessão (preserva user_id)
      operationId: cloneSession
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/compact:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Session Ops (Headless)]
      summary: Compactar contexto da sessão
      operationId: compactSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/export:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Session Ops (Headless)]
      summary: Exportar sessão
      operationId: exportSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/resume:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Session Ops (Headless)]
      summary: Retomar sessão
      operationId: resumeSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Git ──
  /v1/workspace/{session_id}/git/status:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Git (Headless)]
      summary: Status do Git
      operationId: gitStatus
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/diff:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Git (Headless)]
      summary: Diff do Git
      operationId: gitDiff
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/log:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Git (Headless)]
      summary: Log do Git
      operationId: gitLog
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/branches:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    get:
      tags: [Git (Headless)]
      summary: Listar branches
      operationId: gitBranches
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/checkout:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Git (Headless)]
      summary: Checkout de branch
      operationId: gitCheckout
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/commit:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Git (Headless)]
      summary: Criar commit
      operationId: gitCommit
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/branch/create:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Git (Headless)]
      summary: Criar branch
      operationId: gitBranchCreate
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/worktree/create:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Git (Headless)]
      summary: Criar worktree
      operationId: gitWorktreeCreate
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/workspace/{session_id}/git/pr/create:
    parameters: [{ $ref: "#/components/parameters/WorkspaceSessionId" }]
    post:
      tags: [Git (Headless)]
      summary: Criar Pull Request
      operationId: gitPrCreate
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Commands ──
  /v1/commands:
    get:
      tags: [Commands (Headless)]
      summary: Listar slash-commands disponíveis
      operationId: listCommands
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/commands/run:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Sessions]
      summary: Executar um slash-command na sessão
      description: |
        Executa qualquer slash-command (`/status`, `/diff`, `/commit`, …) na
        sessão, montando a string `/{name} {args}` e roteando para o serviço
        equivalente do backend. O `name` vem **sem** a barra inicial.

        A resposta tem **3 variantes** conforme o tier do comando:

        **Tier 1 — síncrono (`{ output }`).** O comando produz texto/dados na
        hora e devolve `{ "output": "<texto>" }`. Comandos Tier 1:
        `/help`, `/status`, `/cost`, `/stats`, `/diff`, `/branch`, `/worktree`,
        `/memory`, `/model`, `/permissions`, `/config`, `/budget`, `/cache`,
        `/session` (list), `/plugins` (alias `/plugin`, `/marketplace`),
        `/agents`, `/skills`, `/version`, `/export`, `/locale`, `/compact`,
        `/tools`, `/buddy`, `/collection`, `/unlock`, `/run`.
        (`/compact` também persiste o histórico compactado de volta na sessão;
        `/export` devolve o payload de exportação no lugar de `{output}`.)

        **Tier 2 — inicia um turno do agente (`{ turn_id }`).** O comando é um
        template de prompt que dispara um turno: a resposta traz apenas o
        `turn_id` e os passos (texto, thinking, tool calls, `turn_completed`)
        chegam de forma assíncrona via SSE em
        `GET /v1/sessions/{id}/events` (correlacione pelo `turn_id`). Se já
        houver um turno ativo, a mensagem entra na fila (FIFO). Comandos Tier 2:
        `/bughunter`, `/ultraplan`, `/commit`, `/commit-push-pr`, `/pr`,
        `/issue`.

        **Tier 3 — somente-cliente (`{ status: "client_only", … }`).** O comando
        é uma ação local de UI/efeito do cliente, sem semântica de serviço; o
        backend devolve `status: "client_only"` com o nome do comando e uma
        mensagem explicando onde a ação realmente acontece. Comandos Tier 3:
        `/clear`, `/resume`, `/update`, `/init`, `/verify`, `/dream`,
        `/teleport`, `/debug-tool-call`, e `/session switch`.

        Comandos não reconhecidos devolvem `{ status: "not_implemented", command }`.
      operationId: runCommand
      security: [{ clerkJwt: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: |
                    Nome do slash-command **sem** a barra (ex.: `status`,
                    `diff`, `commit`).
                  example: status
                args:
                  type: string
                  nullable: true
                  description: |
                    Argumentos do comando como string única (ex.: `create
                    feature/x` para `/branch`). Opcional.
                  example: "--porcelain"
      responses:
        "200":
          description: |
            Comando executado. O corpo é uma das 3 variantes (Tier 1/2/3).
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    title: Tier1Output
                    required: [output]
                    properties:
                      output:
                        type: string
                        description: Texto/dados síncronos do comando (Tier 1).
                  - type: object
                    title: Tier2TurnStarted
                    required: [turn_id]
                    properties:
                      turn_id:
                        type: string
                        description: |
                          ULID do turno iniciado (Tier 2). Acompanhe os passos
                          via SSE em `GET /v1/sessions/{id}/events`. Pode vir no
                          formato `queued:<posição>` se a sessão já tinha um
                          turno ativo.
                  - type: object
                    title: Tier3ClientOnly
                    required: [status, command, message]
                    properties:
                      status:
                        type: string
                        enum: [client_only]
                      command:
                        type: string
                        description: O comando (com barra) que é client-only.
                        example: "/clear"
                      message:
                        type: string
                        description: Explicação de onde a ação acontece no cliente.
                  - type: object
                    title: NotImplemented
                    required: [status, command]
                    properties:
                      status:
                        type: string
                        enum: [not_implemented]
                      command:
                        type: string
              examples:
                tier1:
                  summary: Tier 1 — síncrono ({output})
                  value: { output: "Status\n  Sessão  abc…\n  Modelo  …" }
                tier2:
                  summary: Tier 2 — turno iniciado ({turn_id})
                  value: { turn_id: "01J9Z…" }
                tier3:
                  summary: Tier 3 — somente-cliente
                  value:
                    status: client_only
                    command: "/clear"
                    message: "limpa o histórico visível no cliente (REPL/TUI). Para descartar o contexto no servidor, crie uma nova conversa."
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/commands/compact:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Commands (Headless)]
      summary: Compactar sessão via comando
      operationId: commandCompactSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/commands/export:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Commands (Headless)]
      summary: Exportar sessão via comando
      operationId: commandExportSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/sessions/{id}/commands/resume:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Commands (Headless)]
      summary: Retomar sessão via comando
      operationId: commandResumeSession
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Tasks ──
  /v1/tasks:
    get:
      tags: [Tasks (Headless)]
      summary: Listar tarefas
      operationId: listTasks
      security: [{ serverToken: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/tasks/{id}:
    parameters: [{ $ref: "#/components/parameters/TaskId" }]
    get:
      tags: [Tasks (Headless)]
      summary: Obter tarefa
      operationId: getTask
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/tasks/{id}/output:
    parameters: [{ $ref: "#/components/parameters/TaskId" }]
    get:
      tags: [Tasks (Headless)]
      summary: Saída da tarefa
      operationId: getTaskOutput
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/tasks/{id}/cancel:
    parameters: [{ $ref: "#/components/parameters/TaskId" }]
    post:
      tags: [Tasks (Headless)]
      summary: Cancelar tarefa
      operationId: cancelTask
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Models & Providers ──
  /v1/models:
    get:
      tags: [Models & Providers]
      summary: Listar modelos LLM disponíveis
      operationId: listModels
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/providers:
    get:
      tags: [Models & Providers]
      summary: Listar providers LLM
      operationId: listProviders
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Tools ──
  /v1/tools:
    get:
      tags: [Tools (Headless)]
      summary: Listar todas as tools disponíveis
      operationId: listTools
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/tools/exec:
    post:
      tags: [Tools (Headless)]
      summary: Executar tool cloud-required (delegação sidecar local → cloud)
      description: |
        Executa uma tool que exige ambiente cloud (web/research/conexões/kanban).
        Usado pela delegação P2: o sidecar local encaminha tools cloud-required
        para a VPS. Rejeita (400) tools que não são cloud-required.
      operationId: execTool
      security: [{ serverToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, description: "Nome da tool cloud-required." }
                input: { type: object, description: "Argumentos da tool." }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/tools/{name}:
    parameters: [{ $ref: "#/components/parameters/SkillName" }]
    get:
      tags: [Tools (Headless)]
      summary: Detalhe de uma tool
      operationId: getTool
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/tools/allow:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Tools (Headless)]
      summary: Permitir tools por padrão na sessão
      operationId: toolsAllow
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/sessions/{id}/tools/deny:
    parameters: [{ $ref: "#/components/parameters/SessionId" }]
    post:
      tags: [Tools (Headless)]
      summary: Bloquear tools na sessão
      operationId: toolsDeny
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/tools/rate-limit:
    get:
      tags: [Tools (Headless)]
      summary: Status de rate limit das tools
      operationId: toolsRateLimit
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Telemetry ──
  /v1/telemetry:
    get:
      tags: [Telemetry (Headless)]
      summary: Eventos de telemetria
      operationId: getTelemetry
      security: [{ serverToken: [] }]
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/usage/summary:
    get:
      tags: [Telemetry (Headless)]
      summary: Sumário de uso
      operationId: getUsageSummary
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Cache ──
  /v1/cache/stats:
    get:
      tags: [Cache (Headless)]
      summary: Estatísticas de cache
      operationId: cacheStats
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/cache/clear:
    post:
      tags: [Cache (Headless)]
      summary: Limpar cache
      operationId: cacheClear
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── MCP ──
  /v1/mcp/servers:
    get:
      tags: [MCP (Headless)]
      summary: Listar servidores MCP
      operationId: listMcpServers
      security: [{ serverToken: [] }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { type: object }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [MCP (Headless)]
      summary: Adicionar servidor MCP
      operationId: addMcpServer
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/mcp/servers/{name}:
    parameters: [{ $ref: "#/components/parameters/McpServerName" }]
    put:
      tags: [MCP (Headless)]
      summary: Atualizar servidor MCP
      operationId: updateMcpServer
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [MCP (Headless)]
      summary: Remover servidor MCP
      operationId: deleteMcpServer
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/mcp/servers/{name}/restart:
    parameters: [{ $ref: "#/components/parameters/McpServerName" }]
    post:
      tags: [MCP (Headless)]
      summary: Reiniciar servidor MCP
      operationId: restartMcpServer
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/mcp/servers/{name}/tools:
    parameters: [{ $ref: "#/components/parameters/McpServerName" }]
    get:
      tags: [MCP (Headless)]
      summary: Listar tools do servidor MCP
      operationId: listMcpServerTools
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/mcp/servers/{name}/resources:
    parameters: [{ $ref: "#/components/parameters/McpServerName" }]
    get:
      tags: [MCP (Headless)]
      summary: Listar resources do servidor MCP
      operationId: listMcpServerResources
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/mcp/servers/{name}/tools/{tool}/call:
    parameters:
      - { $ref: "#/components/parameters/McpServerName" }
      - { $ref: "#/components/parameters/McpToolName" }
    post:
      tags: [MCP (Headless)]
      summary: Chamar tool MCP
      operationId: callMcpTool
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Plugins & Agents & Hooks ──
  /v1/plugins:
    get:
      tags: [Plugins (Headless)]
      summary: Listar plugins
      operationId: listPlugins
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/plugins/{name}:
    parameters: [{ $ref: "#/components/parameters/PluginName" }]
    get:
      tags: [Plugins (Headless)]
      summary: Detalhe do plugin
      operationId: getPluginDetail
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Plugins (Headless)]
      summary: Instalar plugin
      operationId: installPlugin
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    put:
      tags: [Plugins (Headless)]
      summary: Atualizar plugin
      operationId: updatePlugin
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Plugins (Headless)]
      summary: Desinstalar plugin
      operationId: uninstallPlugin
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/plugins/{name}/publish-request:
    parameters: [{ $ref: "#/components/parameters/PluginName" }]
    post:
      tags: [Plugins (Headless)]
      summary: Solicitar publicação de plugin user-created no catálogo Dukk
      description: |
        V1 sem tabela DB: o handler loga a solicitação (structured log,
        procurável via `journalctl … | grep plugin_publish_request`) e
        responde `202 Accepted`.
      operationId: pluginPublishRequest
      security: [{ serverToken: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                notes: { type: string, description: "Notas da solicitação (máx. 4 KB)." }
      responses:
        "202": { description: "Solicitação aceita (log estruturado)." }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/agents:
    get:
      tags: [Plugins (Headless)]
      summary: Listar agentes
      operationId: listAgents
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/agents/{name}/run:
    parameters: [{ $ref: "#/components/parameters/AgentName" }]
    post:
      tags: [Plugins (Headless)]
      summary: Executar agente (NÃO IMPLEMENTADO — stub)
      description: |
        **NÃO IMPLEMENTADO.** Atualmente é um stub: o handler ignora o
        parâmetro `name` e qualquer corpo enviado, e sempre responde com
        `501 Not Implemented` e o JSON fixo abaixo. O pipeline de execução de
        agentes ainda não está conectado ao runtime do server.

        ```json
        {
          "error": "not_implemented",
          "message": "agent execution pipeline not wired in server runtime"
        }
        ```
      operationId: runAgent
      security: [{ serverToken: [] }]
      requestBody:
        required: false
        description: Ignorado pelo stub atual.
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "501":
          description: Não implementado (stub fixo).
          content:
            application/json:
              schema:
                type: object
                required: [error, message]
                properties:
                  error:
                    type: string
                    example: not_implemented
                  message:
                    type: string
                    example: agent execution pipeline not wired in server runtime
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/hooks:
    get:
      tags: [Plugins (Headless)]
      summary: Listar hooks
      operationId: listHooks
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    put:
      tags: [Plugins (Headless)]
      summary: Atualizar hooks
      operationId: updateHooks
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── User Commands ──
  /v1/user-commands:
    get:
      tags: [User Commands (Headless)]
      summary: Listar comandos customizados
      operationId: listUserCommands
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [User Commands (Headless)]
      summary: Criar comando customizado
      operationId: createUserCommand
      security: [{ serverToken: [] }]
      responses:
        "201": { $ref: "#/components/responses/Created" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/user-commands/{name}:
    parameters: [{ $ref: "#/components/parameters/UserCommandName" }]
    put:
      tags: [User Commands (Headless)]
      summary: Atualizar comando
      operationId: updateUserCommand
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [User Commands (Headless)]
      summary: Deletar comando
      operationId: deleteUserCommand
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Auth ──
  /v1/auth/status:
    get:
      tags: [Auth (Headless)]
      summary: Status de autenticação
      operationId: getAuthStatus
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/methods:
    get:
      tags: [Auth (Headless)]
      summary: Métodos de auth disponíveis
      operationId: listAuthMethods
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/api-key:
    post:
      tags: [Auth (Headless)]
      summary: Registrar API key
      operationId: setApiKey
      security: [{ serverToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, key]
              properties:
                provider: { type: string }
                key: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/api-key/{provider}:
    parameters: [{ $ref: "#/components/parameters/AuthProvider" }]
    delete:
      tags: [Auth (Headless)]
      summary: Remover API key
      operationId: deleteApiKey
      security: [{ serverToken: [] }]
      responses:
        "204": { $ref: "#/components/responses/NoContent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/auth/oauth/start:
    post:
      tags: [Auth (Headless)]
      summary: Iniciar fluxo OAuth
      operationId: oauthStart
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/oauth/callback:
    get:
      tags: [Auth (Headless)]
      summary: Callback OAuth
      operationId: oauthCallback
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/oauth/refresh:
    post:
      tags: [Auth (Headless)]
      summary: Refresh token OAuth
      operationId: oauthRefresh
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/import/claude-code:
    post:
      tags: [Auth (Headless)]
      summary: Importar credenciais do Claude Code
      operationId: importClaudeCode
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/auth/import/codex:
    post:
      tags: [Auth (Headless)]
      summary: Importar credenciais do Codex
      operationId: importCodex
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  # ── Config ──
  /v1/config:
    get:
      tags: [Config (Headless)]
      summary: Configuração completa
      operationId: getConfig
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Config (Headless)]
      summary: Atualizar configuração
      operationId: patchConfig
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/config/sources:
    get:
      tags: [Config (Headless)]
      summary: Fontes de configuração
      operationId: getConfigSources
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/providers/{id}/test:
    parameters: [{ $ref: "#/components/parameters/ProviderId" }]
    post:
      tags: [Config (Headless)]
      summary: Testar conexão com provider LLM
      operationId: testProvider
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/budget:
    get:
      tags: [Config (Headless)]
      summary: Configuração de budget
      operationId: getBudget
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Config (Headless)]
      summary: Atualizar budget
      operationId: patchBudget
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/theme:
    get:
      tags: [Config (Headless)]
      summary: Configuração de tema
      operationId: getTheme
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Config (Headless)]
      summary: Atualizar tema
      operationId: patchTheme
      security: [{ serverToken: [] }]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
