feat(phase-6): WS3+WS4+WS6 — Analytics, API Tiers, AGNTCY Compliance

WS3 — Advanced Analytics Dashboard:
- DB migration: analytics_events table (tenant_id, date, metric_type, count)
- AnalyticsService: recordEvent (fire-and-forget), getTokenTrend, getAgentActivity, getAgentUsageSummary
- Analytics hooks in OAuth2Service (token_issued) and AgentService (agent_registered/deactivated)
- AnalyticsController + routes/analytics.ts (gated by ANALYTICS_ENABLED flag)
- Portal: TokenTrendChart (recharts LineChart), AgentHeatmap (recharts heatmap), /analytics page

WS4 — API Gateway Tiers:
- DB migration: tenant_tiers table; src/config/tiers.ts (free/pro/enterprise limits)
- TierService: getStatus, initiateUpgrade (Stripe), applyUpgrade; TierLimitError in errors.ts
- tierEnforcement middleware (Redis-backed daily call/token counters; TIER_ENFORCEMENT flag)
- Agent count enforcement in AgentService.create()
- Stripe webhook updated to call TierService.applyUpgrade() on checkout.session.completed
- TierController + routes/tiers.ts; Portal: /settings/tier page with upgrade flow

WS6 — AGNTCY Compliance Certification:
- ComplianceService: generateReport() (Redis-cached 5 min), exportAgentCards()
- Compliance sections: agent-identity (DID + credential expiry checks), audit-trail (Merkle chain)
- ComplianceController updated with getComplianceReport, exportAgentCards handlers
- routes/compliance.ts: new AGNTCY routes (gated by COMPLIANCE_ENABLED flag); SOC2 routes unaffected

QA:
- 28 new unit tests: AnalyticsService (8), TierService (9), ComplianceService (11) — all pass
- 673 total unit tests passing; 0 TypeScript errors across API and portal
- AGNTCY conformance test suite at tests/agntcy-conformance/ (4 protocol tests)
- Portal builds cleanly: 9 routes including /analytics and /settings/tier
- Feature flags verified: ANALYTICS_ENABLED, TIER_ENFORCEMENT, COMPLIANCE_ENABLED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-04 02:20:09 +00:00
parent 0fad328329
commit eea885db04
34 changed files with 4262 additions and 25 deletions

View File

@@ -8,6 +8,7 @@ import { CredentialRepository } from '../repositories/CredentialRepository.js';
import { AuditService } from './AuditService.js';
import { DIDService } from './DIDService.js';
import { EventPublisher } from './EventPublisher.js';
import { AnalyticsService } from './AnalyticsService.js';
import {
IAgent,
ICreateAgentRequest,
@@ -22,6 +23,7 @@ import {
FreeTierLimitError,
} from '../utils/errors.js';
import { agentsRegisteredTotal } from '../metrics/registry.js';
import { TierService } from './TierService.js';
const FREE_TIER_MAX_AGENTS = 100;
@@ -39,6 +41,10 @@ export class AgentService {
* (backward-compatible default).
* @param eventPublisher - Optional EventPublisher. When provided, lifecycle events are
* published as webhooks and Kafka messages (fire-and-forget).
* @param analyticsService - Optional AnalyticsService. When provided, agent_registered
* and agent_deactivated events are recorded fire-and-forget.
* @param tierService - Optional TierService. When provided, per-tier agent count limits
* are enforced at agent creation time (Phase 6 WS4).
*/
constructor(
private readonly agentRepository: AgentRepository,
@@ -46,6 +52,8 @@ export class AgentService {
private readonly auditService: AuditService,
private readonly didService: DIDService | null = null,
private readonly eventPublisher: EventPublisher | null = null,
private readonly analyticsService: AnalyticsService | null = null,
private readonly tierService: TierService | null = null,
) {}
/**
@@ -64,7 +72,17 @@ export class AgentService {
ipAddress: string,
userAgent: string,
): Promise<IAgent> {
// Enforce free-tier agent count limit
const orgId = data.organizationId ?? 'org_system';
// ── Tier-based agent count enforcement (Phase 6 WS4) ────────────────────
// When TierService is available and TIER_ENFORCEMENT is enabled, validate
// the per-tier agent limit for the requesting organization.
if (this.tierService !== null && process.env['TIER_ENFORCEMENT'] !== 'false') {
const tier = await this.tierService.fetchTier(orgId);
await this.tierService.enforceAgentLimit(orgId, tier);
}
// Enforce legacy free-tier agent count limit (global across all orgs)
const currentCount = await this.agentRepository.countActive();
if (currentCount >= FREE_TIER_MAX_AGENTS) {
throw new FreeTierLimitError(
@@ -83,8 +101,7 @@ export class AgentService {
// Generate a W3C DID for the new agent when DIDService is available
if (this.didService !== null) {
const organizationId = data.organizationId ?? 'org_system';
await this.didService.generateDIDForAgent(agent.agentId, organizationId);
await this.didService.generateDIDForAgent(agent.agentId, orgId);
}
// Synchronous audit insert
@@ -100,6 +117,17 @@ export class AgentService {
// Instrument: count successful agent registrations
agentsRegisteredTotal.inc({ deployment_env: data.deploymentEnv });
// Analytics: record agent_registered event (fire-and-forget)
if (this.analyticsService !== null) {
void this.analyticsService.recordEvent(
agent.organizationId ?? 'org_system',
'agent_registered',
).catch((err: unknown) => {
// eslint-disable-next-line no-console
console.error('[AgentService] analytics record (agent_registered) failed', err);
});
}
// Publish event (fire-and-forget)
void this.eventPublisher?.publishEvent(
agent.organizationId,
@@ -263,6 +291,17 @@ export class AgentService {
{},
);
// Analytics: record agent_deactivated event (fire-and-forget)
if (this.analyticsService !== null) {
void this.analyticsService.recordEvent(
agent.organizationId ?? 'org_system',
'agent_deactivated',
).catch((err: unknown) => {
// eslint-disable-next-line no-console
console.error('[AgentService] analytics record (agent_deactivated) failed', err);
});
}
// Publish event (fire-and-forget)
void this.eventPublisher?.publishEvent(
agent.organizationId,