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

@@ -383,7 +383,7 @@ export async function createApp(): Promise<Application> {
// Phase 5 WS5: Scaffold Generator
// ────────────────────────────────────────────────────────────────
const scaffoldService = new ScaffoldService();
const scaffoldController = new ScaffoldController(scaffoldService, pool, auditService);
const scaffoldController = new ScaffoldController(scaffoldService, agentRepo, credentialRepo, auditService);
app.use(`${API_BASE}`, createScaffoldRouter(scaffoldController, authMiddleware));
// ────────────────────────────────────────────────────────────────

View File

@@ -14,7 +14,6 @@
*/
import { Request, Response } from 'express';
import { Pool } from 'pg';
/** Timeout applied to each individual health-check probe (ms). */
const PROBE_TIMEOUT_MS = 3000;
@@ -39,12 +38,18 @@ export interface DetailedHealthResponse {
services: Record<string, ServiceHealthResult>;
}
/** Minimal interface for a PostgreSQL liveness probe. */
export interface DbProbe {
/** Checks DB liveness. Resolves when connected, rejects on failure. */
checkLiveness(): Promise<void>;
}
/**
* Dependencies injected into the controller.
* All fields are optional — services are only probed when their client is provided.
*/
export interface HealthDetailedDeps {
pool: Pool;
dbProbe: DbProbe;
/** Optional Vault URL — when provided, the controller probes Vault's /v1/sys/health. */
vaultAddr?: string;
/** Optional OPA URL — when provided, the controller probes OPA's /health. */
@@ -90,13 +95,13 @@ async function runProbe(
* optional-service clients. The `handle` method is an Express route handler.
*/
export class HealthDetailedController {
private readonly pool: Pool;
private readonly dbProbe: DbProbe;
private readonly vaultAddr: string | undefined;
private readonly opaUrl: string | undefined;
private readonly redisClient: { ping(): Promise<string> } | null;
constructor(deps: HealthDetailedDeps) {
this.pool = deps.pool;
this.dbProbe = deps.dbProbe;
this.vaultAddr = deps.vaultAddr;
this.opaUrl = deps.opaUrl;
this.redisClient = deps.redisClient ?? null;
@@ -118,12 +123,7 @@ export class HealthDetailedController {
// ── PostgreSQL probe ────────────────────────────────────────────────────
services['postgres'] = await runProbe(async () => {
const start = Date.now();
const client = await this.pool.connect();
try {
await client.query('SELECT 1');
} finally {
client.release();
}
await this.dbProbe.checkLiveness();
return Date.now() - start;
});

View File

@@ -4,7 +4,8 @@
*/
import { Request, Response, NextFunction } from 'express';
import { Pool } from 'pg';
import { AgentRepository } from '../repositories/AgentRepository.js';
import { CredentialRepository } from '../repositories/CredentialRepository.js';
import { AuditService } from '../services/AuditService.js';
import { ScaffoldService, SCAFFOLD_LANGUAGES } from '../services/ScaffoldService.js';
import { ScaffoldLanguage } from '../types/scaffold.js';
@@ -17,12 +18,14 @@ import { AgentNotFoundError, AuthorizationError, ValidationError } from '../util
export class ScaffoldController {
/**
* @param scaffoldService - The scaffold generation service.
* @param pool - PostgreSQL connection pool for agent credential lookup.
* @param agentRepo - Agent repository for agent lookup.
* @param credentialRepo - Credential repository for active credential lookup.
* @param auditService - Audit log service.
*/
constructor(
private readonly scaffoldService: ScaffoldService,
private readonly pool: Pool,
private readonly agentRepo: AgentRepository,
private readonly credentialRepo: CredentialRepository,
private readonly auditService: AuditService,
) {}
@@ -53,38 +56,25 @@ export class ScaffoldController {
const tenantId = req.user.organization_id ?? 'org_system';
// Fetch agent and verify it belongs to the authenticated tenant
const agentResult = await this.pool.query<{
agent_id: string;
email: string;
organization_id: string;
}>(
`SELECT agent_id, email, organization_id FROM agents WHERE agent_id = $1`,
[agentId],
);
const agent = await this.agentRepo.findById(agentId);
if (agentResult.rows.length === 0) {
if (agent === null) {
throw new AgentNotFoundError(agentId);
}
const agentRow = agentResult.rows[0];
if (agentRow.organization_id !== tenantId) {
if (agent.organizationId !== tenantId) {
throw new AuthorizationError('You do not own this agent.');
}
// Fetch the agent's active credential client_id
const credResult = await this.pool.query<{ client_id: string }>(
`SELECT client_id FROM credentials WHERE agent_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`,
[agentId],
);
const clientId =
credResult.rows.length > 0 ? credResult.rows[0].client_id : agentRow.agent_id;
const activeClientId = await this.credentialRepo.findActiveClientId(agentId);
const clientId = activeClientId ?? agentId;
const apiUrl = process.env['API_URL'] ?? process.env['NEXT_PUBLIC_API_URL'] ?? 'https://api.sentryagent.ai';
const { stream, filename } = await this.scaffoldService.generateScaffold({
agentId,
agentName: agentRow.email.split('@')[0] ?? agentId,
agentName: agent.email.split('@')[0] ?? agentId,
clientId,
language,
apiUrl,

View File

@@ -79,23 +79,23 @@ export function getPool(): Pool {
}
});
// Wrap pool.query to record duration in Prometheus.
// The pg Pool.query method is heavily overloaded — the only safe approach
// without TypeScript errors is a typed-any wrapper on the shim itself.
// We capture originalQuery as `(...args: any[]) => Promise<any>` to satisfy
// TypeScript's spread-into-rest constraint; this is the one sanctioned use of
// `any` in this file.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalQuery = pool.query.bind(pool) as (...args: any[]) => Promise<any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(pool as any).query = async (...args: any[]): Promise<any> => {
const end = dbQueryDurationSeconds.startTimer({ operation: 'query' });
try {
return await originalQuery(...args);
} finally {
end();
}
};
// Wrap pool.query to record Prometheus query duration.
// Uses unknown[] + Object.defineProperty to avoid `any` while preserving
// the pool's typed interface for all callers (TypeScript still sees Pool['query']).
const originalQuery = pool.query.bind(pool);
Object.defineProperty(pool, 'query', {
value: async (...args: unknown[]): Promise<unknown> => {
const end = dbQueryDurationSeconds.startTimer({ operation: 'query' });
try {
return await (originalQuery as (...a: unknown[]) => Promise<unknown>)(...args);
} finally {
end();
}
},
writable: true,
configurable: true,
enumerable: false,
});
}
return pool;
}

View File

@@ -250,4 +250,18 @@ export class CredentialRepository {
);
return result.rowCount ?? 0;
}
/**
* Finds the client_id of the most recent active credential for an agent.
*
* @param agentId - The agent UUID.
* @returns The client_id string, or null if no active credential exists.
*/
async findActiveClientId(agentId: string): Promise<string | null> {
const result: QueryResult<{ client_id: string }> = await this.pool.query(
`SELECT client_id FROM credentials WHERE agent_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`,
[agentId],
);
return result.rows.length > 0 ? (result.rows[0].client_id ?? null) : null;
}
}

View File

@@ -10,7 +10,7 @@
import { Router, Request, Response } from 'express';
import { Pool } from 'pg';
import { RedisClientType } from 'redis';
import { HealthDetailedController } from '../controllers/HealthDetailedController.js';
import { DbProbe, HealthDetailedController } from '../controllers/HealthDetailedController.js';
/** Response shape for GET /health */
interface HealthResponse {
@@ -33,9 +33,21 @@ interface HealthResponse {
export function createHealthRouter(pool: Pool, redis: RedisClientType): Router {
const router = Router();
// Create a minimal DbProbe adapter — keeps raw Pool out of the controller.
const dbProbe: DbProbe = {
async checkLiveness(): Promise<void> {
const client = await pool.connect();
try {
await client.query('SELECT 1');
} finally {
client.release();
}
},
};
// Instantiate the detailed health controller with optional service clients.
const detailedController = new HealthDetailedController({
pool,
dbProbe,
redisClient: redis,
vaultAddr: process.env['VAULT_ADDR'] ?? undefined,
opaUrl: process.env['OPA_URL'] ?? undefined,