fix(vv): resolve all 6 V&V issues — field trial unblocked
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>
This commit is contained in:
228
docs/openapi/oidc-token-exchange.yaml
Normal file
228
docs/openapi/oidc-token-exchange.yaml
Normal file
@@ -0,0 +1,228 @@
|
||||
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'
|
||||
Reference in New Issue
Block a user