openapi: 3.1.0
info:
  title: multipliers.ai Public API
  version: 1.0.0
  summary: AI operating layer with a per-tenant contextual Brain — public API surface
  description: |
    The multipliers.ai public API exposes the **Brain** (per-tenant
    contextual substrate) and **agent invocation** routes that
    hyperscalar-deployed AI runtimes call.

    ## Architecture

    multipliers.ai is an AI operating layer with three layers:

    1. **Brain (centralized, per-tenant)** — `composeContext`, vector
       retrieval, episodic memory, skill registry. Stays on
       multipliers.ai SaaS or in customer VPC.
    2. **Agent reasoning (portable)** — Tier-A pipelines (win-prob,
       comparable deals, multi-tier compose, etc.). Runs in
       multipliers SaaS by default; deployable to customer's chosen
       hyperscalar AI platform.
    3. **Hyperscalar deployment template** — CloudFormation /
       Terraform / ARM that creates a Lambda / Cloud Function /
       Azure Function in the customer's cloud, calling our Brain +
       agent invoke routes via the public API documented here.

    ## Auth

    Two schemes:

    - `firebaseBearer` — Firebase ID token. Used by internal callers
      (multipliers.ai dashboard, admin UI). Most external integrators
      should use the tenant-token approach.
    - `tenantTokenBearer` — Tenant API token (`mt_*` format) issued
      via `POST /api/admin/tenants/{tenantId}/api-tokens`. Carries
      scope (e.g. `brain`, `embedded_ui`) and optional agent binding.
      Used by hyperscalar-deployed agents and any tool-calling LLM
      client.

    Many Brain routes accept EITHER scheme. Admin / write routes
    (skill CRUD, plan management, agent admin) accept ONLY
    `firebaseBearer` + admin role — tokens cannot reach them by
    construction.

    ## Hyperscalar tool registration

    - **AWS Bedrock Agent**: register an Action Group with
      `apiSchema=https://multipliers.ai/api/openapi.yaml`
    - **Vertex AI Agent Builder**: `OpenApiTool(open_api_spec_url=
      "https://multipliers.ai/api/openapi.yaml")`
    - **Azure OpenAI Service**: convert via `openapi-generator` to
      function-tool spec, register on the Assistants API
    - **Claude API / OpenAI Assistants direct**: hand-author tool
      definitions for the 2-3 endpoints needed, generate from this
      spec via community tools

    ## Versioning

    `info.version` is semver. Breaking changes require a parallel
    `/api/v2/*` path with ≥6mo overlap before removing v1. Adding
    optional fields or new endpoints bumps the minor version.

  contact:
    name: multipliers.ai support
    url: https://multipliers.ai/services
    email: support@multipliers.ai
  license:
    name: Proprietary
    url: https://multipliers.ai/legal

servers:
  - url: https://multipliers.ai
    description: Production
  - url: https://staging.multipliers.ai
    description: Staging (where applicable)

tags:
  - name: Brain
    description: |
      Per-tenant contextual substrate. Cross-system entity context,
      vector retrieval over ingested knowledge, episodic memory,
      skill registry. The keystone of multipliers.ai.
  - name: Agents
    description: |
      Direct agent invocation routes — hyperscalar-deployed runtimes
      (Bedrock Agent action groups, Vertex AI tools, Azure OpenAI
      function tools) call these via the tool-calling spec
      generated from this OpenAPI document.
  - name: Admin
    description: |
      Tenant API token CRUD. Multipliers admins call these to issue
      / list / revoke the bearer tokens used by hyperscalar-deployed
      agents.

components:
  securitySchemes:
    firebaseBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Firebase-issued ID token. Used by internal callers
        (multipliers.ai dashboard, admin UI). Most external
        integrators should use tenantTokenBearer instead.
    tenantTokenBearer:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: |
        Tenant API token issued via POST /api/admin/tenants/{tenantId}/api-tokens.
        Format: `mt_<64-hex>`. Carries scope (e.g. `brain`,
        `embedded_ui`) and optional agent binding. Used by
        hyperscalar-deployed agents (AWS Bedrock Lambda, Vertex AI
        Cloud Function, Azure AI Function App) and any tool-calling
        LLM client (Claude API, OpenAI Assistants direct).

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: Authentication required
        detail:
          type: string

    EntityRef:
      type: object
      required: [kind, id]
      properties:
        kind:
          type: string
          description: Canonical entity kind. v1 accepts account, opportunity, order, incident, product, contact + any string (handled as pending-impl per-lens).
          example: account
        id:
          type: string
          description: Stable id within the kind. v1 caller-supplied (system-native ids ok). v2 will be the canonical id resolved by the Entity Model.
          example: 0011x000003ABCDEFA1
        system:
          type: string
          description: Optional system hint (e.g. salesforce, servicenow, shopify) — v2 will use this for entity resolution.
          example: salesforce

    Lens:
      type: string
      enum: [commercial, support, comms, ops, health, decisions]

    LensSlice:
      type: object
      required: [lens, status, chunks, events, metrics, sources, freshness]
      properties:
        lens:
          $ref: '#/components/schemas/Lens'
        status:
          type: string
          enum: [ok, partial, no-data, error, pending-impl]
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/ContextChunk'
        events:
          type: array
          items:
            $ref: '#/components/schemas/ContextEvent'
        metrics:
          type: object
          description: 3M.1 Semantic Layer placeholder; empty in v1.
        sources:
          type: array
          items:
            $ref: '#/components/schemas/ContextSource'
        freshness:
          type: object
          properties:
            newestObservedAt:
              type: string
              format: date-time
              nullable: true
            lagSeconds:
              type: integer
              nullable: true
        note:
          type: string

    ContextChunk:
      type: object
      required: [id, text, isRedacted, score, metadata, source]
      properties:
        id:
          type: string
        text:
          type: string
        isRedacted:
          type: boolean
        score:
          type: number
          description: Cosine similarity (0..1)
        metadata:
          type: object
        source:
          type: object
          nullable: true
          properties:
            uri:
              type: string
              nullable: true
            observedAt:
              type: string
              format: date-time
            kind:
              type: string
            source:
              type: string

    ContextEvent:
      type: object
      required: [id, kind, source, observedAt, payload]
      properties:
        id:
          type: string
        kind:
          type: string
          example: order.orchestrated
        source:
          type: string
          example: agent:order-orchestration
        observedAt:
          type: string
          format: date-time
        payload:
          type: object

    ContextSource:
      type: object
      required: [source, itemCount]
      properties:
        source:
          type: string
        itemCount:
          type: integer
        newestObservedAt:
          type: string
          format: date-time
          nullable: true

    ComposeContextRequest:
      type: object
      required: [entity, lenses]
      properties:
        entity:
          $ref: '#/components/schemas/EntityRef'
        lenses:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Lens'
        asOf:
          type: string
          format: date-time
          description: Point-in-time view; defaults to now.
        topK:
          oneOf:
            - type: integer
            - type: object
              additionalProperties:
                type: integer
        query:
          type: string
          description: Optional NL hint; biases vector retrieval per-lens.
        taskScope:
          type: object
          description: WS-3K task scope filters (caller's narrowed effective view).

    ContextBundle:
      type: object
      required: [entity, asOf, lenses, scope, latencyMs, perLensLatencyMs]
      properties:
        entity:
          $ref: '#/components/schemas/EntityRef'
        asOf:
          type: string
          format: date-time
        lenses:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/LensSlice'
        scope:
          type: object
          required: [tenantId, role, unrestricted]
          properties:
            tenantId:
              type: string
            role:
              type: string
            unrestricted:
              type: boolean
        latencyMs:
          type: integer
        perLensLatencyMs:
          type: object
          additionalProperties:
            type: integer

    RetrieveRequest:
      type: object
      required: [query]
      properties:
        query:
          type: string
        topK:
          type: integer
          default: 10
        expandGraph:
          type: boolean
          default: false
        metadataMatch:
          type: object
        embeddingModel:
          type: string
        taskScope:
          type: object

    RetrieveResponse:
      type: object
      required: [chunks]
      properties:
        chunks:
          type: array
          items:
            $ref: '#/components/schemas/ContextChunk'
        entities:
          type: array
        edges:
          type: array
        scope:
          type: object
        embeddingModel:
          type: string
        latencyMs:
          type: integer

    InvokeSkillRequest:
      type: object
      required: [input]
      properties:
        input:
          type: object
        version:
          type: string
          description: Optional version pin. Defaults to currentVersion.

    InvokeSkillResponse:
      type: object
      required: [invocationId, status]
      properties:
        invocationId:
          type: string
        status:
          type: string
          enum: [success, failed, timeout, escalated]
        output:
          type: object
        error:
          type: object

    RecordOutcomeRequest:
      type: object
      required: [outcome]
      properties:
        outcome:
          type: object
          description: Operator's actual outcome — feeds the WS-3L learning loop.
        notes:
          type: string

    QuoteCoPilotInvokeBody:
      type: object
      required: [opportunityId]
      properties:
        opportunityId:
          type: string
          example: 0061x000003ABCDEFA1
        overrides:
          type: object
          properties:
            forceDryRun:
              type: boolean
              description: When true, agent composes + validates but does NOT persist a Quote.
            forceTierEmitCount:
              type: integer
              minimum: 1
              maximum: 3
            forceCompositionMode:
              type: string
              enum: [fully-generative, rep-scoped]
        sfdcUserId:
          type: string
        sfdcOrgId:
          type: string
        correlationId:
          type: string
        idempotencyKey:
          type: string

    QuoteCoPilotInvokeResponse:
      type: object
      required: [status, runId, correlationId]
      properties:
        status:
          type: string
          enum: [success, escalated, refused]
        runId:
          type: string
        correlationId:
          type: string
        primary:
          $ref: '#/components/schemas/TierWireShape'
        alternatives:
          type: array
          items:
            $ref: '#/components/schemas/TierWireShape'
        approvalState:
          type: object
          properties:
            status:
              type: string
            rationalePackage:
              type: string
        tenantBrainContext:
          type: object
          description: 3M.11a retrofit signals.
          properties:
            asOf:
              type: string
              format: date-time
            lenses:
              type: array
              items:
                type: string
            signals:
              type: object
              properties:
                priorOrderOrchestrationCount:
                  type: integer
                openSupportTicketCount:
                  type: integer
                priorOrchestrationFailureCount:
                  type: integer
        message:
          type: string
          description: Set when status=escalated or refused
        reason:
          type: string
        nextSteps:
          type: string

    TierWireShape:
      type: object
      required: [quoteId, quoteSummary, rationale]
      properties:
        quoteId:
          type: string
          description: SFDC Quote.Id of the persisted quote (live mode). Reps deep-link via externalUrl.
        externalUrl:
          type: string
          format: uri
          description: Direct link to /lightning/r/Quote/{Id}/view (omitted in preview/dryRun mode).
        tier:
          type: string
          enum: [good, better, best]
        confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
        quoteSummary:
          type: object
          required: [lineCount, totalAmountMinor, currencyCode, weightedDiscountPct]
          properties:
            lineCount:
              type: integer
            totalAmountMinor:
              type: string
              description: Bigint serialized as string. Divide by 100 for major units in 2-decimal currencies.
            currencyCode:
              type: string
            weightedDiscountPct:
              type: number
              format: float
            validityDays:
              type: integer
            termMonths:
              type: integer
            renewalCohortMatch:
              type: boolean
            bundleFitScore:
              type: number
              format: float
        rationale:
          type: object
          properties:
            winProbability:
              type: number
              format: float
            posture:
              type: string
              enum: [strong, balanced, defended, weak]
            termAlignment:
              type: string
            summary:
              type: string

    OrchestrateInput:
      type: object
      required: [opportunityId]
      properties:
        opportunityId:
          type: string
        amendVersion:
          type: integer
          minimum: 1
        correlationId:
          type: string
        idempotencyKey:
          type: string

    OrchestrateResult:
      type: object
      required: [runId, status, correlationId, opportunityId, amendVersion, plan, outcomes, latencyMs, completedAt]
      properties:
        runId:
          type: string
        status:
          type: string
          enum: [success, no-op, validation-failed, partial, failed]
        correlationId:
          type: string
        opportunityId:
          type: string
        amendVersion:
          type: integer
        plan:
          type: array
          items:
            $ref: '#/components/schemas/OrderGroup'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/GroupOutcome'
        failures:
          type: array
          items:
            type: object
            properties:
              rule:
                type: string
                enum: [credit-hold, stage-not-closed-won, no-line-items, opp-not-found, quote-not-found]
              detail:
                type: string
        latencyMs:
          type: integer
        contextSummary:
          type: object
        completedAt:
          type: string
          format: date-time

    OrderGroup:
      type: object
      required: [groupKey, lineItemIds, idempotencyKey]
      properties:
        groupKey:
          type: string
        serviceLocation:
          type: string
          nullable: true
        lineItemIds:
          type: array
          items:
            type: string
        productFamily:
          type: string
        idempotencyKey:
          type: string

    GroupOutcome:
      type: object
      required: [groupKey, status]
      properties:
        groupKey:
          type: string
        status:
          type: string
          enum: [created, idempotent-skip, snow-failed, sfdc-writeback-failed]
        scRequestNumber:
          type: string
        scRequestSysId:
          type: string
        sfOrderId:
          type: string
        error:
          type: string

    IssueTokenRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 255
          description: Human-readable label for the token (e.g. "Acme prod Bedrock agent").
        scope:
          type: string
          default: embedded_ui
          description: Scope this token can satisfy. Examples - brain, embedded_ui, embedded_ui:salesforce.
        agentId:
          type: string
          nullable: true
          description: Optional agent binding. When set, this token can ONLY invoke the named agent.
        ttlDays:
          type: integer
          minimum: 1
          maximum: 365
          description: Optional expiry. When omitted, token never expires (still revocable).

    IssuedToken:
      type: object
      required: [token, tokenId, tenantId, name, scope, createdAt]
      properties:
        token:
          type: string
          description: |
            Plaintext token. SHOWN ONLY ONCE at creation; never
            retrievable again. Save immediately, paste into Secrets
            Manager / Secret Manager / Key Vault.
          example: mt_a1b2c3d4e5f6...
        tokenId:
          type: string
          format: uuid
        tenantId:
          type: string
        name:
          type: string
        scope:
          type: string
        agentId:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
        _warning:
          type: string

    TokenSummary:
      type: object
      required: [tokenId, tenantId, name, scope, createdAt, revoked]
      properties:
        tokenId:
          type: string
        tenantId:
          type: string
        name:
          type: string
        scope:
          type: string
        agentId:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        revoked:
          type: boolean

  responses:
    Unauthorized:
      description: Missing, invalid, or revoked authentication.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Token scope or user role insufficient for this route.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found (or not owned by caller's tenant).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadRequest:
      description: Request body validation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: Internal server error. Capture correlation_id from response headers (when present) for support.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

paths:
  /api/brain/compose-context:
    post:
      tags: [Brain]
      summary: Fetch entity-scoped multi-lens context bundle
      description: |
        Returns a structured bundle of cross-system context for a
        canonical entity (account, opportunity, order, incident, etc.).
        Lenses (commercial / support / comms / ops / health / decisions)
        each return relevant chunks + events + metrics + freshness.

        **Use case:** A Bedrock-deployed agent calling this endpoint
        gets the same per-tenant context the multipliers SaaS dashboard
        sees — including cross-system signals from prior agent
        invocations (e.g. order.orchestrated events, support tickets).
      operationId: composeContext
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ComposeContextRequest'
      responses:
        '200':
          description: Context bundle
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContextBundle'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalError' }

  /api/brain/retrieve:
    post:
      tags: [Brain]
      summary: Vector retrieval over tenant brain_chunks
      description: Embeds query + returns top-K most similar chunks scoped to caller's tenant. Optional graph expansion.
      operationId: retrieve
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RetrieveRequest'
      responses:
        '200':
          description: Retrieved chunks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RetrieveResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '500': { $ref: '#/components/responses/InternalError' }

  /api/brain/invoke-skill/{domain}/{name}:
    post:
      tags: [Brain]
      summary: Schema-validated skill invocation
      description: |
        Resolves <domain>/<name>@<pin> to a registered skill, validates
        input against the persisted JSON schema, executes when the Tier-2
        SkillModule is loaded (else returns escalated). Persists invocation
        row regardless.
      operationId: invokeSkill
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string }
          example: sales
        - name: name
          in: path
          required: true
          schema: { type: string }
          example: compute_lead_score
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvokeSkillRequest'
      responses:
        '200':
          description: Invocation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvokeSkillResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '500': { $ref: '#/components/responses/InternalError' }

  /api/brain/invocations/{id}/record-outcome:
    post:
      tags: [Brain]
      summary: Record the operator's outcome for a prior invocation
      description: Closes the WS-3L learning loop. Updates the invocation row's outcome jsonb + emits a brain_event for cross-agent corroboration.
      operationId: recordInvocationOutcome
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordOutcomeRequest'
      responses:
        '200':
          description: Outcome recorded
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/brain/episodes:
    post:
      tags: [Brain]
      summary: Record an episodic memory entry (AP.12)
      operationId: recordEpisode
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                Per AP.12 schema. Includes agentTemplate, subjectKind,
                subjectId, scenarioFingerprint, action, outcome, etc.
      responses:
        '200':
          description: Episode recorded
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/brain/episodes/{agentTemplate}/{subjectKind}/{subjectId}:
    get:
      tags: [Brain]
      summary: Retrieve episodic memory for a specific subject
      description: Returns prior episodes the named agent ran for this subject. Used by agents to ground their reasoning in their own history.
      operationId: retrieveEpisodes
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      parameters:
        - { name: agentTemplate, in: path, required: true, schema: { type: string } }
        - { name: subjectKind, in: path, required: true, schema: { type: string } }
        - { name: subjectId, in: path, required: true, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: Episodes
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/brain/episodes/{agentTemplate}:
    get:
      tags: [Brain]
      summary: Cohort/pattern-mine episodes across subjects
      operationId: retrieveEpisodesByTemplate
      security:
        - firebaseBearer: []
        - tenantTokenBearer: []
      parameters:
        - { name: agentTemplate, in: path, required: true, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
      responses:
        '200':
          description: Episodes
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /api/agents/quote-co-pilot/invoke:
    post:
      tags: [Agents]
      summary: Invoke Quote Co-Pilot
      description: |
        Generates 1-3 quote tier candidates for an SFDC Opportunity.
        In live mode (default), the orchestrator's `qsor.submitQuote`
        step persists each tier as a Draft Quote with QuoteLineItems
        in the customer's Salesforce org. In preview mode
        (`overrides.forceDryRun=true`), candidates are composed +
        validated but NOT persisted.

        Pricing canon enforced server-side: no_pricebook_writes,
        no_silent_currency_conversion, no_status_approved,
        approval-gate. Cannot be bypassed.
      operationId: invokeQuoteCoPilot
      security:
        - tenantTokenBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteCoPilotInvokeBody'
      responses:
        '200':
          description: Quote envelope (success / escalated / refused)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteCoPilotInvokeResponse'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500': { $ref: '#/components/responses/InternalError' }

  /api/agents/order-orchestration/run:
    post:
      tags: [Agents]
      summary: Run Order Orchestration on a closed-won SFDC opportunity
      description: |
        Reads the opp + quote + line items, validates (credit-hold,
        stage='Closed Won', has line items), decomposes into per-site
        groups, writes ServiceNow `sc_request` records (real connector
        in active sequence step #3; in-memory stub today), writes back
        to SFDC Order with custom field SnowRequest__c, emits Brain
        events tagged with entityKind='opportunity' so future
        composeContext calls surface them.

        Auth: currently Firebase only; gains tenantTokenBearer support
        alongside the SFDC Pub/Sub trigger task (active sequence #5).
      operationId: runOrderOrchestration
      security:
        - firebaseBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrchestrateInput'
      responses:
        '200':
          description: Orchestration result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrchestrateResult'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '500': { $ref: '#/components/responses/InternalError' }

  /api/admin/tenants/{tenantId}/api-tokens:
    post:
      tags: [Admin]
      summary: Issue a tenant API token
      description: |
        Creates a new tenant-scoped bearer token. **Plaintext returned
        ONCE in the response — save immediately, it cannot be retrieved
        again.** Hash is persisted server-side; verify is constant-time.

        Use case - issue a token bound to a specific agent + scope so
        it can only invoke that agent or hit Brain endpoints.
      operationId: issueTenantApiToken
      security:
        - firebaseBearer: []
      parameters:
        - { name: tenantId, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssueTokenRequest'
      responses:
        '201':
          description: Token issued (plaintext shown once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedToken'
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

    get:
      tags: [Admin]
      summary: List tenant API tokens (no plaintext)
      operationId: listTenantApiTokens
      security:
        - firebaseBearer: []
      parameters:
        - { name: tenantId, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Tokens
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens:
                    type: array
                    items:
                      $ref: '#/components/schemas/TokenSummary'

  /api/admin/tenants/{tenantId}/api-tokens/{id}:
    delete:
      tags: [Admin]
      summary: Revoke a tenant API token (soft-revoke)
      description: Sets `revoked_at`. Subsequent verify attempts return null. Cannot be reversed; issue a new token instead.
      operationId: revokeTenantApiToken
      security:
        - firebaseBearer: []
      parameters:
        - { name: tenantId, in: path, required: true, schema: { type: string } }
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Optional revocation reason for audit.
      responses:
        '200':
          description: Token revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean
                  tokenId:
                    type: string
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
