All findings from the inaugural LeadValidator audit resolved and confirmed. Release gate: PASS. VV_ISSUE_002 (BLOCKER): 15 OpenAPI specs verified present covering all 20 route groups (46 endpoints documented in docs/openapi/) VV_ISSUE_003 (MAJOR): Remove any types from src/db/pool.ts — replaced pool.query shim with unknown[] + Object.defineProperty, zero any types, eslint-disable suppressions removed VV_ISSUE_004 (MAJOR): Remove raw Pool from ScaffoldController and HealthDetailedController — injected AgentRepository/CredentialRepository and DbProbe interface respectively; added CredentialRepository.findActiveClientId() VV_ISSUE_005 (MAJOR): Add unit tests for 5 untested services — ComplianceStatusStore, EventPublisher, MarketplaceService, OIDCTrustPolicyService, UsageService VV_ISSUE_006 (MAJOR): Add integration tests for 7 missing route groups — analytics, billing, tiers, webhooks, marketplace, oidc-trust-policies, oidc-token-exchange VV_ISSUE_001 (MINOR): Create missing design.md and tasks.md in 4 OpenSpec archives — all archives now complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
229 lines
8.9 KiB
YAML
229 lines
8.9 KiB
YAML
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'
|