openapi: "3.0.3"

info:
  title: SentryAgent.ai — OIDC Token Exchange (Workload Identity Federation)
  version: 1.0.0
  description: |
    OIDC Token Exchange endpoint for Workload Identity Federation on the
    SentryAgent.ai AgentIdP platform.

    This endpoint allows CI/CD workflows (e.g. GitHub Actions) to exchange their
    short-lived OIDC JWT for a SentryAgent.ai Bearer access token without requiring
    long-lived credentials stored as secrets.

    **Flow:**
    1. Tenant creates a trust policy via `POST /api/v1/oidc/trust-policies`
       linking a GitHub repo + optional branch to an agent UUID.
    2. In the GitHub Actions workflow, fetch the OIDC token:
       ```
       id-token: write  # required permissions block
       OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
         "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value)
       ```
    3. POST the GitHub OIDC token to `POST /api/v1/oidc/token`.
    4. Receive a SentryAgent.ai Bearer token valid for 1 hour.

    **This endpoint is unauthenticated** — the GitHub OIDC JWT IS the credential.
    Trust-policy enforcement is performed server-side.

servers:
  - url: http://localhost:3000/api/v1
    description: Local development server
  - url: https://api.sentryagent.ai/v1
    description: Production server

tags:
  - name: OIDC Token Exchange
    description: Workload Identity Federation token exchange (unauthenticated)

components:
  schemas:
    OIDCTokenExchangeRequest:
      type: object
      description: |
        Request body for exchanging a GitHub OIDC JWT for a SentryAgent.ai access token.
      required:
        - token
      properties:
        token:
          type: string
          description: |
            The GitHub OIDC JWT obtained from the GitHub Actions token request endpoint.
            Must be a valid, unexpired JWT signed by GitHub.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZXBvOmFjbWUtY29ycC9hZ2VudC1kZXBsb3llcjpyZWY6cmVmcy9oZWFkcy9tYWluIiwiaXNzIjoiaHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImV4cCI6MTc0MzE1NDgwMH0.signature"

    OIDCTokenExchangeResponse:
      type: object
      description: SentryAgent.ai Bearer access token issued in exchange for the OIDC JWT.
      required:
        - access_token
        - token_type
        - expires_in
        - scope
      properties:
        access_token:
          type: string
          description: |
            Signed JWT access token. Use as `Authorization: Bearer <access_token>`
            in subsequent API calls.
          example: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
        token_type:
          type: string
          enum:
            - Bearer
          description: Token type. Always `Bearer`.
          example: "Bearer"
        expires_in:
          type: integer
          description: Token lifetime in seconds from issuance. Default is `3600`.
          example: 3600
        scope:
          type: string
          description: Space-separated OAuth 2.0 scopes granted by this token.
          example: "agents:read agents:write"

    ErrorResponse:
      type: object
      description: Standard error response envelope.
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: "TRUST_POLICY_NOT_FOUND"
        message:
          type: string
          example: "No trust policy matches the provided OIDC token claims."
        details:
          type: object
          additionalProperties: true

  responses:
    InternalServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            code: "INTERNAL_SERVER_ERROR"
            message: "An unexpected error occurred. Please try again later."

paths:
  /oidc/token:
    post:
      operationId: exchangeOIDCToken
      tags:
        - OIDC Token Exchange
      summary: Exchange a GitHub OIDC JWT for a SentryAgent.ai access token
      description: |
        Exchanges a GitHub Actions OIDC JWT for a SentryAgent.ai Bearer access token.

        **Verification steps performed server-side:**
        1. Decode and validate the GitHub OIDC JWT signature against GitHub's JWKS.
        2. Verify the token is not expired.
        3. Match the token's `sub` claim (format: `repo:<org>/<repo>:ref:refs/heads/<branch>`)
           against registered trust policies.
        4. Apply branch constraint if the matching policy has one.
        5. Verify the linked agent is active and not decommissioned.
        6. Issue a SentryAgent.ai access token scoped to the linked agent.

        **This endpoint is unauthenticated** — the GitHub OIDC JWT serves as the credential.

        **GitHub Actions example:**
        ```yaml
        permissions:
          id-token: write
          contents: read

        steps:
          - name: Get OIDC token and exchange
            run: |
              GITHUB_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
                "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api.sentryagent.ai" | jq -r .value)
              AGENT_TOKEN=$(curl -sX POST https://api.sentryagent.ai/api/v1/oidc/token \
                -H 'Content-Type: application/json' \
                -d "{\"token\": \"$GITHUB_TOKEN\"}" | jq -r .access_token)
        ```
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OIDCTokenExchangeRequest'
            example:
              token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyZXBvOmFjbWUtY29ycC9hZ2VudC1kZXBsb3llcjpyZWY6cmVmcy9oZWFkcy9tYWluIiwiaXNzIjoiaHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbSJ9.signature"
      responses:
        '200':
          description: SentryAgent.ai access token issued successfully.
          headers:
            Cache-Control:
              schema:
                type: string
              description: Token responses must not be cached.
              example: "no-store"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OIDCTokenExchangeResponse'
              example:
                access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature"
                token_type: "Bearer"
                expires_in: 3600
                scope: "agents:read agents:write"
        '400':
          description: Missing or malformed `token` field in request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                code: "VALIDATION_ERROR"
                message: "The 'token' field is required."
        '401':
          description: The provided OIDC token is invalid, expired, or has an invalid signature.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                expired:
                  summary: Token is expired
                  value:
                    code: "OIDC_TOKEN_EXPIRED"
                    message: "The provided GitHub OIDC token has expired."
                invalidSignature:
                  summary: Invalid signature
                  value:
                    code: "OIDC_TOKEN_INVALID"
                    message: "The provided GitHub OIDC token signature is invalid."
        '403':
          description: Token claims do not match any active trust policy, or the linked agent is inactive.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                noPolicy:
                  summary: No matching trust policy
                  value:
                    code: "TRUST_POLICY_NOT_FOUND"
                    message: "No trust policy matches the provided OIDC token claims."
                branchMismatch:
                  summary: Branch constraint not satisfied
                  value:
                    code: "TRUST_POLICY_BRANCH_MISMATCH"
                    message: "The token's branch does not match the trust policy constraint."
                    details:
                      allowed: "main"
                      provided: "feature/xyz"
                agentSuspended:
                  summary: Linked agent is suspended
                  value:
                    code: "AGENT_SUSPENDED"
                    message: "The agent linked to this trust policy is currently suspended."
        '500':
          $ref: '#/components/responses/InternalServerError'
