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:
SentryAgent.ai Developer
2026-04-07 04:52:47 +00:00
parent d216096dfb
commit 7441c9f298
49 changed files with 8954 additions and 70 deletions

View 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'