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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user