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 ` 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:/:ref:refs/heads/`) 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'